ai_rspec_writer 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +175 -0
- data/bin/ai_rspec_writer +48 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/ai_rspec_writer/ai_providers/chatgpt_generator.rb +54 -0
- data/lib/ai_rspec_writer/ai_providers/gemini_generator.rb +58 -0
- data/lib/ai_rspec_writer/configuration.rb +14 -0
- data/lib/ai_rspec_writer/generator.rb +32 -0
- data/lib/ai_rspec_writer/schema_extractor.rb +50 -0
- data/lib/ai_rspec_writer/spec_file_handler.rb +43 -0
- data/lib/ai_rspec_writer/version.rb +5 -0
- data/lib/ai_rspec_writer.rb +27 -0
- metadata +183 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a627e0f602d1fedd3b92ac52cf455d22a3013f54a1716d969b15723e2fbf7599
|
4
|
+
data.tar.gz: 565e1d42ceba48da1651be58de3cb61c911cc7b39ac971c4082c2c081619b4c2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7c4815991ab61bd9d5eb9c82e184e995d0f8c1fd7da99b9aac63441d12f75c722eaf6c067b893749cf988826d9a759541db5850ab27efce1d03c134506de2639
|
7
|
+
data.tar.gz: 6eb38e5fcf0e733ffcddd57408e870a38ea8e648e2d26ce25a815bb1f698aae52180a6f891828785d4155456ca7d6e771d69e43b4ba5b2b3b48082f3ec06a9dd
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Aniruddha Dilip Mirajkar
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
# ai_rspec_writer
|
2
|
+
|
3
|
+
`ai_rspec_writer` is an AI-powered tool for generating **RSpec test cases** in **Ruby on Rails applications**. It supports **ChatGPT** and **Gemini AI** to create **model, controller, and service tests** automatically, improving test coverage and development efficiency.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## **🚀 Features**
|
8
|
+
- 📝 **Automatically generates RSpec tests** for models, controllers, and services.
|
9
|
+
- 📌 **Extracts database schema** for validation and association tests.
|
10
|
+
- 🛠 **Supports both ChatGPT and Gemini AI**.
|
11
|
+
- 🔍 **Customizable test instructions** using extra comments.
|
12
|
+
- 🏆 **Ensures high code coverage** by generating edge-case scenarios.
|
13
|
+
- 🔑 **Secure API authentication** using OpenAI & Google Gemini keys.
|
14
|
+
|
15
|
+
---
|
16
|
+
|
17
|
+
## **🛠 Installation**
|
18
|
+
### **1️⃣ Add to your Gemfile**
|
19
|
+
```ruby
|
20
|
+
group :development, :test do
|
21
|
+
gem 'ai_rspec_writer'
|
22
|
+
gem 'factory_bot_rails' # Generates dummy data
|
23
|
+
gem 'rails-controller-testing' # Required for controller specs
|
24
|
+
gem 'faker' # Fake data generation
|
25
|
+
gem 'shoulda-matchers', '~> 5.0' # One-liner matchers for RSpec
|
26
|
+
gem 'simplecov', require: false # Code coverage analysis
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
### **2️⃣ Run Bundle Install**
|
31
|
+
```sh
|
32
|
+
bundle install
|
33
|
+
```
|
34
|
+
|
35
|
+
### **3️⃣ Set up API Keys**
|
36
|
+
Create a `.env` file in your project root and add:
|
37
|
+
```
|
38
|
+
CHATGPT_API_KEY=your-openai-api-key
|
39
|
+
GEMINI_API_KEY=your-google-gemini-api-key
|
40
|
+
DEFAULT_AI=chatgpt
|
41
|
+
CHATGPT_MODEL=your-model
|
42
|
+
GEMINI_MODEL=your-model
|
43
|
+
```
|
44
|
+
|
45
|
+
Or export them in your terminal:
|
46
|
+
```sh
|
47
|
+
export CHATGPT_API_KEY="your-openai-api-key"
|
48
|
+
export GEMINI_API_KEY="your-google-gemini-api-key"
|
49
|
+
export DEFAULT_AI="chatgpt"
|
50
|
+
export CHATGPT_MODEL="your-model"
|
51
|
+
export GEMINI_MODEL="your-model"
|
52
|
+
```
|
53
|
+
|
54
|
+
```
|
55
|
+
Default Setting:
|
56
|
+
GEMINI_MODEL=o3-mini
|
57
|
+
CHATGPT_MODEL=gemini-2.0-flash
|
58
|
+
DEFAULT_AI=chatgpt
|
59
|
+
```
|
60
|
+
---
|
61
|
+
|
62
|
+
## **📌 Usage**
|
63
|
+
### **Command Structure**
|
64
|
+
```sh
|
65
|
+
bundle exec ai_rspec_writer -f <file_path> -t <table_names> -e "<extra_comment>" -a <ai_model>
|
66
|
+
```
|
67
|
+
|
68
|
+
### **🔹 Example Commands**
|
69
|
+
#### ✅ Generate RSpec tests using `Gemini AI`
|
70
|
+
```sh
|
71
|
+
bundle exec ai_rspec_writer -f app/controllers/obento/obento_zaikos_controller.rb,app/models/store_notification.rb,app/models/common_setting.rb -t store_notifications,common_settings -e "use devise admin and ensure 100% test coverage" -a gemini
|
72
|
+
```
|
73
|
+
|
74
|
+
#### ✅ Generate RSpec tests using `ChatGPT AI`
|
75
|
+
```sh
|
76
|
+
bundle exec ai_rspec_writer -f app/controllers/obento/obento_zaikos_controller.rb,app/models/store_notification.rb,app/models/common_setting.rb -t store_notifications,common_settings -e "use devise admin and ensure 100% test coverage" -a chatgpt
|
77
|
+
```
|
78
|
+
|
79
|
+
#### ✅ Generate RSpec tests without -a flag
|
80
|
+
If no AI model (-a) is passed, ai_rspec_writer will default to DEFAULT_AI from .env or use ChatGPT if not explicitly set.
|
81
|
+
```sh
|
82
|
+
bundle exec ai_rspec_writer -f app/controllers/obento/obento_zaikos_controller.rb,app/models/store_notification.rb,app/models/common_setting.rb -t store_notifications,common_settings -e "use devise admin and ensure 100% test coverage"
|
83
|
+
```
|
84
|
+
|
85
|
+
---
|
86
|
+
|
87
|
+
## **🔍 Command Options**
|
88
|
+
| **Option** | **Description** | **Example** |
|
89
|
+
|------------|----------------|-------------|
|
90
|
+
| `-f, --files` | Files to generate tests for (**comma-separated** paths). | `-f app/models/user.rb,app/controllers/users_controller.rb` |
|
91
|
+
| `-t, --table_name` | **Database tables** to extract schema info for tests. | `-t users,orders` |
|
92
|
+
| `-e, --ec` | **Extra comments** to customize test behavior. | `-e "use Devise authentication"` |
|
93
|
+
| `-a, --ai` | **AI model** to use (`chatgpt` or `gemini`). | `-a gemini` |
|
94
|
+
|
95
|
+
---
|
96
|
+
|
97
|
+
## **📝 Example Output**
|
98
|
+
After running:
|
99
|
+
```sh
|
100
|
+
bundle exec ai_rspec_writer -f app/models/store_notification.rb -t store_notifications -e "use FactoryBot"
|
101
|
+
```
|
102
|
+
It generates:
|
103
|
+
```ruby
|
104
|
+
# spec/models/store_notification_spec.rb
|
105
|
+
require 'rails_helper'
|
106
|
+
|
107
|
+
RSpec.describe StoreNotification, type: :model do
|
108
|
+
let(:store_notification) { create(:store_notification) }
|
109
|
+
|
110
|
+
describe "Validations" do
|
111
|
+
it { should validate_presence_of(:title) }
|
112
|
+
it { should validate_presence_of(:message) }
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "Associations" do
|
116
|
+
it { should belong_to(:store) }
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "Methods" do
|
120
|
+
it "sends notification successfully" do
|
121
|
+
expect(store_notification.send_notification).to be_truthy
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
---
|
128
|
+
|
129
|
+
## **⚠️ Troubleshooting**
|
130
|
+
### **❌ `ChatGPT API key is missing.`**
|
131
|
+
**Solution:** Set the API key in `.env`:
|
132
|
+
```
|
133
|
+
CHATGPT_API_KEY=your-api-key
|
134
|
+
```
|
135
|
+
Or export it:
|
136
|
+
```sh
|
137
|
+
export CHATGPT_API_KEY="your-api-key"
|
138
|
+
```
|
139
|
+
|
140
|
+
### **❌ `Gemini API key is missing.`**
|
141
|
+
**Solution:** Set the Gemini API key in `.env`:
|
142
|
+
```
|
143
|
+
GEMINI_API_KEY=your-api-key
|
144
|
+
```
|
145
|
+
|
146
|
+
### **❌ `Error: Please provide a file to generate tests for using -f`**
|
147
|
+
**Solution:** Ensure you pass the `-f` option with valid file paths:
|
148
|
+
```sh
|
149
|
+
bundle exec ai_rspec_writer -f app/models/user.rb
|
150
|
+
```
|
151
|
+
|
152
|
+
### **❌ Tests are not covering all cases**
|
153
|
+
**Solution:** Try providing additional **extra comments**:
|
154
|
+
```sh
|
155
|
+
bundle exec ai_rspec_writer -f app/models/user.rb -e "Generate edge case tests"
|
156
|
+
```
|
157
|
+
|
158
|
+
---
|
159
|
+
|
160
|
+
## **📜 License**
|
161
|
+
This project is licensed under the **MIT License**.
|
162
|
+
|
163
|
+
---
|
164
|
+
|
165
|
+
## **👨💻 Contributors**
|
166
|
+
- **Your Name** (@aniruddhami)
|
167
|
+
- **Open for Contributions!** Feel free to fork & improve.
|
168
|
+
|
169
|
+
---
|
170
|
+
|
171
|
+
## **✨ Future Improvements**
|
172
|
+
- 📌 **RSpec test generation improvements**
|
173
|
+
- 📌 **Support for Minitest**
|
174
|
+
- 📌 **AI model selection enhancements**
|
175
|
+
- 📌 **More detailed error handling**
|
data/bin/ai_rspec_writer
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "optparse"
|
6
|
+
require_relative "../lib/ai_rspec_writer"
|
7
|
+
require_relative "../lib/ai_rspec_writer/schema_extractor"
|
8
|
+
|
9
|
+
if AiRspecWriter.configuration.chatgpt_api_key.nil? || AiRspecWriter.configuration.gemini_api_key.nil?
|
10
|
+
puts "❌ ChatGPT API key is missing.".red
|
11
|
+
puts "ℹ️ Set it in `.env`, or use `export CHATGPT_API_KEY=your-key`.".yellow
|
12
|
+
exit 1
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
options = {}
|
17
|
+
|
18
|
+
OptionParser.new do |opts|
|
19
|
+
opts.banner = "Usage: ai_rspec_writer [options]"
|
20
|
+
|
21
|
+
opts.on("-f FILES", "--files=FILES", "Specify the files to generate tests for (comma-separated)(eg main_file,secondary_file,....)") do |files|
|
22
|
+
options[:files] = files.split(",").map(&:strip)
|
23
|
+
end
|
24
|
+
|
25
|
+
opts.on("-t table_names", "--table_name=TABLE_NAME", "Table name to find schema") do |table_names|
|
26
|
+
options[:table_names] = table_names.split(",").map(&:strip)
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("-a AI", "--ai=AI", "Specify AI: chatgpt, gemini (default: chatgpt)") do |ai|
|
30
|
+
options[:ai] = ai
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("-e COMMENT", "--ec=COMMENT", "Add Extra Comment at the time of generating test") do |comment|
|
34
|
+
options[:extra_comment] = comment
|
35
|
+
end
|
36
|
+
end.parse!
|
37
|
+
|
38
|
+
if options[:files]
|
39
|
+
|
40
|
+
schema = AIRspecWriter::SchemaExtractor.extract_schema_from_file(options[:table_names])
|
41
|
+
|
42
|
+
generator = AIRspecWriter::Generator.new(ai: options[:ai], ec: options[:extra_comment], schema: schema)
|
43
|
+
spec_code = generator.generate(options[:files])
|
44
|
+
|
45
|
+
AIRspecWriter::SpecFileHandler.save_spec_file(options[:files][0], spec_code)
|
46
|
+
else
|
47
|
+
puts "❌ Error: Please provide a file to generate tests for using `-f`"
|
48
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "ai_rspec_writer"
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
11
|
+
# require "pry"
|
12
|
+
# Pry.start
|
13
|
+
|
14
|
+
require "irb"
|
15
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openai"
|
4
|
+
require "pry"
|
5
|
+
require "tty-spinner"
|
6
|
+
require "colorize"
|
7
|
+
|
8
|
+
module AIRspecWriter
|
9
|
+
module AIProviders
|
10
|
+
# Generates RSpec tests using OpenAI's ChatGPT API
|
11
|
+
class ChatGPTGenerator
|
12
|
+
def initialize(ec: nil, schema: nil)
|
13
|
+
@extra_comment = ec
|
14
|
+
@schema = schema
|
15
|
+
@client = OpenAI::Client.new(access_token: AiRspecWriter.configuration.chatgpt_api_key)
|
16
|
+
end
|
17
|
+
|
18
|
+
def generate_spec(file_content)
|
19
|
+
my_schema = "check my table defination details: #{@schema}" if @schema
|
20
|
+
|
21
|
+
# puts "Write RSpec tests for this Ruby code with all factory files and rubocop standards:\n\n#{file_content}\n\n#{@extra_comment}\n\n #{my_schema}"
|
22
|
+
|
23
|
+
# Define loading spinner
|
24
|
+
spinner = TTY::Spinner.new("[:spinner] Generating AI-powered RSpec tests".yellow, format: :dots)
|
25
|
+
|
26
|
+
# Start the spinner
|
27
|
+
spinner.auto_spin
|
28
|
+
|
29
|
+
response = @client.chat(
|
30
|
+
parameters: {
|
31
|
+
model: AiRspecWriter.configuration.chatgpt_model,
|
32
|
+
messages: [{ role: "user", content: "Write RSpec tests for this Ruby code with all factory files and rubocop standards:\n\n#{file_content}\n\n#{@extra_comment}\n\n #{my_schema}" }]
|
33
|
+
}
|
34
|
+
)
|
35
|
+
|
36
|
+
# Stop the spinner when AI response is ready
|
37
|
+
spinner.success("✅ Done!")
|
38
|
+
|
39
|
+
full_content = response.dig("choices", 0, "message", "content")
|
40
|
+
|
41
|
+
# extract_ruby_code(full_content)
|
42
|
+
full_content
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Extracts Ruby code from AI response
|
48
|
+
def extract_ruby_code(text)
|
49
|
+
match = text.match(/```ruby\n(.*?)```/m)
|
50
|
+
match ? match[1].strip : "Error: No valid Ruby code detected in AI response."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'gemini-ai'
|
4
|
+
require 'colorize'
|
5
|
+
require 'tty-spinner'
|
6
|
+
|
7
|
+
module AIRspecWriter
|
8
|
+
module AIProviders
|
9
|
+
# Generates RSpec tests using Google's Gemini AI API
|
10
|
+
class GeminiGenerator
|
11
|
+
def initialize(ec: nil, schema: nil)
|
12
|
+
@extra_comment = ec
|
13
|
+
@schema = schema
|
14
|
+
@client = Gemini.new(
|
15
|
+
credentials: {
|
16
|
+
service: 'generative-language-api',
|
17
|
+
api_key: AiRspecWriter.configuration.gemini_api_key
|
18
|
+
},
|
19
|
+
options: { model: AiRspecWriter.configuration.gemini_model, server_sent_events: true }
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_spec(file_content)
|
24
|
+
fixed_promt = "Write RSpec tests for this Ruby code with all factory files and rubocop standards:"
|
25
|
+
my_schema = "Check my table definition details: #{@schema}" if @schema
|
26
|
+
|
27
|
+
# Define loading spinner
|
28
|
+
spinner = TTY::Spinner.new("[:spinner] Generating AI-powered RSpec tests".yellow, format: :dots)
|
29
|
+
|
30
|
+
# Start the spinner
|
31
|
+
spinner.auto_spin
|
32
|
+
|
33
|
+
begin
|
34
|
+
result = @client.generate_content({
|
35
|
+
contents: {
|
36
|
+
role: "user",
|
37
|
+
parts: {
|
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?
|
44
|
+
# Stop the spinner when AI response is ready
|
45
|
+
spinner.success("✅ Done!")
|
46
|
+
return result["candidates"].first["content"]["parts"].first["text"]
|
47
|
+
else
|
48
|
+
spinner.error('(Gemini AI returned an empty response.)')
|
49
|
+
return nil
|
50
|
+
end
|
51
|
+
rescue StandardError => e
|
52
|
+
spinner.error("(❌ An error occurred while calling Gemini AI: #{e.message})")
|
53
|
+
exit 1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# lib/ai_rspec_writer/configuration.rb
|
2
|
+
module AiRspecWriter
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :chatgpt_api_key, :gemini_api_key, :ai_provider, :chatgpt_model, :gemini_model
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@chatgpt_api_key = ENV.fetch("CHATGPT_API_KEY", nil)
|
8
|
+
@gemini_api_key = ENV.fetch("GEMINI_API_KEY", nil)
|
9
|
+
@ai_provider = ENV.fetch("DEFAULT_AI", "chatgpt")
|
10
|
+
@chatgpt_model = ENV.fetch("CHATGPT_MODEL", "o3-mini")
|
11
|
+
@gemini_model = ENV.fetch("GEMINI_MODEL", "gemini-2.0-flash")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "ai_providers/chatgpt_generator"
|
4
|
+
require_relative "ai_providers/gemini_generator"
|
5
|
+
require "pry"
|
6
|
+
|
7
|
+
module AIRspecWriter
|
8
|
+
# Responsible for handling AI-based test generation
|
9
|
+
class Generator
|
10
|
+
def initialize(ai: nil, ec: nil, schema: nil)
|
11
|
+
@ai = ai.presence || AiRspecWriter.configuration.ai_provider
|
12
|
+
@extra_comment = ec
|
13
|
+
@schema = schema
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate(file_paths)
|
17
|
+
file_contents = file_paths.map do |file_path|
|
18
|
+
next "Error: File not found (#{file_path})" unless File.exist?(file_path)
|
19
|
+
File.read(file_path)
|
20
|
+
end.join("\n\n") # Combine multiple files
|
21
|
+
|
22
|
+
generator = case @ai.downcase
|
23
|
+
when "chatgpt" then AIProviders::ChatGPTGenerator.new(ec: @extra_comment, schema: @schema)
|
24
|
+
when "gemini" then AIProviders::GeminiGenerator.new(ec: @extra_comment, schema: @schema)
|
25
|
+
else
|
26
|
+
raise "Unsupported AI. Use 'chatgpt' or 'gemini'."
|
27
|
+
end
|
28
|
+
|
29
|
+
generator.generate_spec(file_contents)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/string" # ✅ Fix: Ensure String#camelize, #underscore, etc., are available
|
4
|
+
require "tty-spinner"
|
5
|
+
require "colorize"
|
6
|
+
|
7
|
+
module AIRspecWriter
|
8
|
+
class SchemaExtractor
|
9
|
+
SCHEMA_FILE = "db/schema.rb"
|
10
|
+
|
11
|
+
def self.extract_schema_from_file(table_names)
|
12
|
+
return nil unless table_names
|
13
|
+
|
14
|
+
# Define loading spinner
|
15
|
+
spinner = TTY::Spinner.new("[:spinner] Fetching Schema From Schema.rb : ".yellow, format: :dots)
|
16
|
+
|
17
|
+
# Start the spinner
|
18
|
+
spinner.auto_spin
|
19
|
+
extract_schema(table_names, spinner)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.extract_schema(table_names, spinner)
|
23
|
+
unless File.exist?(SCHEMA_FILE)
|
24
|
+
spinner.error("\n❌ Error: `db/schema.rb` not found. Run `rails db:migrate` first.")
|
25
|
+
return nil
|
26
|
+
end
|
27
|
+
|
28
|
+
schema_content = File.read(SCHEMA_FILE)
|
29
|
+
|
30
|
+
match = "" # ✅ Fix: Initialize match variable to avoid nil error
|
31
|
+
matched_tables = []
|
32
|
+
table_names.map do |table_name|
|
33
|
+
matched_data = schema_content.match(/create_table "#{table_name}".*?^\s*end$/m)
|
34
|
+
if matched_data.present?
|
35
|
+
match += "#{matched_data[0]}\n\n"
|
36
|
+
matched_tables << table_name
|
37
|
+
end
|
38
|
+
end.join("\n\n") # Combine multiple files
|
39
|
+
|
40
|
+
if match
|
41
|
+
# Stop the spinner when AI response is ready
|
42
|
+
spinner.success("#{matched_tables} ✅ Done!")
|
43
|
+
return match
|
44
|
+
else
|
45
|
+
spinner.error("(No schema found for table: #{table_names})")
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "tty-spinner"
|
5
|
+
require "colorize"
|
6
|
+
|
7
|
+
module AIRspecWriter
|
8
|
+
class SpecFileHandler
|
9
|
+
SPEC_DIRS = {
|
10
|
+
"app/models/" => "spec/models",
|
11
|
+
"app/controllers/" => "spec/controllers"
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def determine_spec_path(file_path)
|
16
|
+
# Ensure we strip out "app/" to avoid double "models/" issue
|
17
|
+
relative_path = file_path.sub(%r{^app/}, "")
|
18
|
+
|
19
|
+
# Find matching folder mapping
|
20
|
+
spec_dir = SPEC_DIRS.find { |prefix, _| relative_path.start_with?(prefix.sub("app/", "")) }
|
21
|
+
spec_folder = spec_dir ? spec_dir[1] : "spec/other"
|
22
|
+
|
23
|
+
# Ensure correct path
|
24
|
+
File.join(spec_folder, relative_path.sub(%r{^models/|controllers/}, "").sub(".rb", "_spec.rb"))
|
25
|
+
end
|
26
|
+
|
27
|
+
def save_spec_file(file_path, spec_code)
|
28
|
+
# Define loading spinner
|
29
|
+
spinner = TTY::Spinner.new("[:spinner] RSpec File Create".yellow, format: :dots)
|
30
|
+
|
31
|
+
output_path = determine_spec_path(file_path)
|
32
|
+
|
33
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
34
|
+
File.write(output_path, spec_code)
|
35
|
+
# Stop the spinner when AI response is ready
|
36
|
+
spinner.success("✅ Done!")
|
37
|
+
puts " - Open the file & review: `code #{output_path}`".light_blue
|
38
|
+
puts " - Run the test: `bundle exec rspec #{output_path}`".light_blue
|
39
|
+
puts " - Refactor if needed & improve test coverage! 🚀\n".green
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative "ai_rspec_writer/configuration"
|
2
|
+
require_relative "ai_rspec_writer/version"
|
3
|
+
require_relative "ai_rspec_writer/generator"
|
4
|
+
require_relative "ai_rspec_writer/spec_file_handler"
|
5
|
+
require_relative "ai_rspec_writer/constants"
|
6
|
+
require "colorize"
|
7
|
+
|
8
|
+
module AiRspecWriter
|
9
|
+
class Error < StandardError; end
|
10
|
+
class << self
|
11
|
+
attr_accessor :configuration
|
12
|
+
|
13
|
+
def configuration
|
14
|
+
@configuration ||= Configuration.new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
env_file = File.expand_path(".env", Dir.pwd)
|
20
|
+
if File.exist?(env_file)
|
21
|
+
# Load environment variables from .env file
|
22
|
+
require "dotenv"
|
23
|
+
Dotenv.load(env_file)
|
24
|
+
end
|
25
|
+
|
26
|
+
# ✅ Ensure configuration is initialized
|
27
|
+
AiRspecWriter.configuration
|
metadata
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ai_rspec_writer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aniruddha Mirajkar
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-03-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec-rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '6.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '6.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: ruby-openai
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '7.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '7.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: httparty
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.21'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.21'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: tty-spinner
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.9'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: gemini-ai
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.2.0
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 4.2.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: dotenv
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.1'
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: pry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.14.2
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.14.2
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rubocop
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.50'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.50'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rubocop-performance
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.19'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.19'
|
139
|
+
description: AI-powered RSpec test generator that creates model and controller specs.
|
140
|
+
email:
|
141
|
+
- mirajkaraniruddha@gmail.com
|
142
|
+
executables:
|
143
|
+
- ai_rspec_writer
|
144
|
+
extensions: []
|
145
|
+
extra_rdoc_files: []
|
146
|
+
files:
|
147
|
+
- LICENSE
|
148
|
+
- README.md
|
149
|
+
- bin/ai_rspec_writer
|
150
|
+
- bin/console
|
151
|
+
- bin/setup
|
152
|
+
- lib/ai_rspec_writer.rb
|
153
|
+
- lib/ai_rspec_writer/ai_providers/chatgpt_generator.rb
|
154
|
+
- lib/ai_rspec_writer/ai_providers/gemini_generator.rb
|
155
|
+
- lib/ai_rspec_writer/configuration.rb
|
156
|
+
- lib/ai_rspec_writer/generator.rb
|
157
|
+
- lib/ai_rspec_writer/schema_extractor.rb
|
158
|
+
- lib/ai_rspec_writer/spec_file_handler.rb
|
159
|
+
- lib/ai_rspec_writer/version.rb
|
160
|
+
homepage: https://github.com/aniruddhami/ai_rspec_writer
|
161
|
+
licenses:
|
162
|
+
- MIT
|
163
|
+
metadata: {}
|
164
|
+
post_install_message:
|
165
|
+
rdoc_options: []
|
166
|
+
require_paths:
|
167
|
+
- lib
|
168
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: 3.0.0
|
173
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
174
|
+
requirements:
|
175
|
+
- - ">="
|
176
|
+
- !ruby/object:Gem::Version
|
177
|
+
version: '0'
|
178
|
+
requirements: []
|
179
|
+
rubygems_version: 3.4.1
|
180
|
+
signing_key:
|
181
|
+
specification_version: 4
|
182
|
+
summary: A gem to generate RSpec tests using AI (ChatGPT, GeminiAI).
|
183
|
+
test_files: []
|