rails_skills 0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +99 -0
- data/Rakefile +5 -0
- data/lib/generators/rails_skills/commands_library/quality.md +16 -0
- data/lib/generators/rails_skills/install/install_generator.rb +211 -0
- data/lib/generators/rails_skills/install/templates/agents/api-dev.md.tt +28 -0
- data/lib/generators/rails_skills/install/templates/agents/fullstack-dev.md.tt +28 -0
- data/lib/generators/rails_skills/install/templates/agents/rails-developer.md.tt +22 -0
- data/lib/generators/rails_skills/install/templates/settings.local.json.tt +11 -0
- data/lib/generators/rails_skills/rules_library/code-style.md +28 -0
- data/lib/generators/rails_skills/rules_library/security.md +33 -0
- data/lib/generators/rails_skills/rules_library/testing.md +27 -0
- data/lib/generators/rails_skills/skill/skill_generator.rb +46 -0
- data/lib/generators/rails_skills/skill/templates/SKILL.md.tt +29 -0
- data/lib/generators/rails_skills/skills_library/rails-api-controllers/SKILL.md +106 -0
- data/lib/generators/rails_skills/skills_library/rails-controllers/SKILL.md +125 -0
- data/lib/generators/rails_skills/skills_library/rails-hotwire/SKILL.md +89 -0
- data/lib/generators/rails_skills/skills_library/rails-jobs/SKILL.md +39 -0
- data/lib/generators/rails_skills/skills_library/rails-models/SKILL.md +105 -0
- data/lib/generators/rails_skills/skills_library/rails-views/SKILL.md +109 -0
- data/lib/generators/rails_skills/skills_library/rspec-testing/SKILL.md +105 -0
- data/lib/rails_skills/railtie.rb +10 -0
- data/lib/rails_skills/version.rb +5 -0
- data/lib/rails_skills.rb +12 -0
- metadata +86 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-api-controllers
|
|
3
|
+
description: API-only controllers, serialization, authentication, versioning
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails API Controllers
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
| Pattern | Example |
|
|
12
|
+
|---------|---------|
|
|
13
|
+
| **Base class** | `class Api::V1::PostsController < ActionController::API` |
|
|
14
|
+
| **Render JSON** | `render json: @post, status: :ok` |
|
|
15
|
+
| **Error response** | `render json: { error: "Not found" }, status: :not_found` |
|
|
16
|
+
| **Pagination** | `@posts = Post.page(params[:page]).per(25)` |
|
|
17
|
+
|
|
18
|
+
## API Controller Structure
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
module Api
|
|
22
|
+
module V1
|
|
23
|
+
class PostsController < ApplicationController
|
|
24
|
+
def index
|
|
25
|
+
posts = Post.order(created_at: :desc).page(params[:page])
|
|
26
|
+
render json: posts
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def show
|
|
30
|
+
post = Post.find(params[:id])
|
|
31
|
+
render json: post
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def create
|
|
35
|
+
post = Post.new(post_params)
|
|
36
|
+
|
|
37
|
+
if post.save
|
|
38
|
+
render json: post, status: :created
|
|
39
|
+
else
|
|
40
|
+
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def update
|
|
45
|
+
post = Post.find(params[:id])
|
|
46
|
+
|
|
47
|
+
if post.update(post_params)
|
|
48
|
+
render json: post
|
|
49
|
+
else
|
|
50
|
+
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def destroy
|
|
55
|
+
post = Post.find(params[:id])
|
|
56
|
+
post.destroy
|
|
57
|
+
head :no_content
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def post_params
|
|
63
|
+
params.require(:post).permit(:title, :body, :published)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## API Routing
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
Rails.application.routes.draw do
|
|
74
|
+
namespace :api do
|
|
75
|
+
namespace :v1 do
|
|
76
|
+
resources :posts
|
|
77
|
+
resources :users, only: [:index, :show]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Error Handling
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
module Api
|
|
87
|
+
class ApplicationController < ActionController::API
|
|
88
|
+
rescue_from ActiveRecord::RecordNotFound do |e|
|
|
89
|
+
render json: { error: e.message }, status: :not_found
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
rescue_from ActionController::ParameterMissing do |e|
|
|
93
|
+
render json: { error: e.message }, status: :bad_request
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Best Practices
|
|
100
|
+
|
|
101
|
+
1. Use API-only base class (`ActionController::API`)
|
|
102
|
+
2. Version your API with namespaces
|
|
103
|
+
3. Return consistent error formats
|
|
104
|
+
4. Use proper HTTP status codes
|
|
105
|
+
5. Paginate list endpoints
|
|
106
|
+
6. Use serializers for complex JSON responses
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-controllers
|
|
3
|
+
description: Controller actions, routing, REST conventions, filters, strong parameters
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Controllers
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
| Pattern | Example |
|
|
12
|
+
|---------|---------|
|
|
13
|
+
| **Generate** | `rails g controller Posts index show` |
|
|
14
|
+
| **Route** | `resources :posts` |
|
|
15
|
+
| **Filter** | `before_action :set_post, only: [:show, :edit, :update, :destroy]` |
|
|
16
|
+
| **Strong params** | `params.require(:post).permit(:title, :body)` |
|
|
17
|
+
| **Redirect** | `redirect_to @post, notice: "Created!"` |
|
|
18
|
+
| **Render** | `render :new, status: :unprocessable_entity` |
|
|
19
|
+
|
|
20
|
+
## Controller Structure
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class PostsController < ApplicationController
|
|
24
|
+
before_action :set_post, only: [:show, :edit, :update, :destroy]
|
|
25
|
+
|
|
26
|
+
def index
|
|
27
|
+
@posts = Post.all.order(created_at: :desc)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def show; end
|
|
31
|
+
|
|
32
|
+
def new
|
|
33
|
+
@post = Post.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def create
|
|
37
|
+
@post = Post.new(post_params)
|
|
38
|
+
|
|
39
|
+
if @post.save
|
|
40
|
+
redirect_to @post, notice: "Post created."
|
|
41
|
+
else
|
|
42
|
+
render :new, status: :unprocessable_entity
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def edit; end
|
|
47
|
+
|
|
48
|
+
def update
|
|
49
|
+
if @post.update(post_params)
|
|
50
|
+
redirect_to @post, notice: "Post updated."
|
|
51
|
+
else
|
|
52
|
+
render :edit, status: :unprocessable_entity
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def destroy
|
|
57
|
+
@post.destroy
|
|
58
|
+
redirect_to posts_path, notice: "Post deleted."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def set_post
|
|
64
|
+
@post = Post.find(params[:id])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def post_params
|
|
68
|
+
params.require(:post).permit(:title, :body, :published)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Routing
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
Rails.application.routes.draw do
|
|
77
|
+
resources :posts
|
|
78
|
+
resources :posts, only: [:index, :show]
|
|
79
|
+
|
|
80
|
+
resources :authors do
|
|
81
|
+
resources :posts, shallow: true
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
resources :posts do
|
|
85
|
+
member { post :publish }
|
|
86
|
+
collection { get :archived }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
namespace :admin do
|
|
90
|
+
resources :users
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Response Formats
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
respond_to do |format|
|
|
99
|
+
format.html { redirect_to @post }
|
|
100
|
+
format.json { render json: @post, status: :created }
|
|
101
|
+
format.turbo_stream
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Error Handling
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
class ApplicationController < ActionController::Base
|
|
109
|
+
rescue_from ActiveRecord::RecordNotFound, with: :not_found
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def not_found
|
|
114
|
+
render file: Rails.root.join("public/404.html"), status: :not_found
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Best Practices
|
|
120
|
+
|
|
121
|
+
1. Keep controllers thin - move logic to models or service objects
|
|
122
|
+
2. Always use strong parameters
|
|
123
|
+
3. Use `before_action` for shared setup
|
|
124
|
+
4. Return proper HTTP status codes
|
|
125
|
+
5. Follow RESTful conventions
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-hotwire
|
|
3
|
+
description: Turbo Drive, Turbo Frames, Turbo Streams, and Stimulus
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Hotwire
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
| Pattern | Example |
|
|
12
|
+
|---------|---------|
|
|
13
|
+
| **Turbo Frame** | `<%= turbo_frame_tag "name" do %>` |
|
|
14
|
+
| **Turbo Stream** | `<%= turbo_stream.append "list" do %>` |
|
|
15
|
+
| **Stimulus controller** | `data-controller="toggle"` |
|
|
16
|
+
| **Stimulus action** | `data-action="click->toggle#switch"` |
|
|
17
|
+
| **Stimulus target** | `data-toggle-target="content"` |
|
|
18
|
+
|
|
19
|
+
## Turbo Frames
|
|
20
|
+
|
|
21
|
+
```erb
|
|
22
|
+
<%# Wrap content in a frame %>
|
|
23
|
+
<%= turbo_frame_tag "post_#{@post.id}" do %>
|
|
24
|
+
<h2><%= @post.title %></h2>
|
|
25
|
+
<%= link_to "Edit", edit_post_path(@post) %>
|
|
26
|
+
<% end %>
|
|
27
|
+
|
|
28
|
+
<%# Lazy-loaded frame %>
|
|
29
|
+
<%= turbo_frame_tag "comments", src: post_comments_path(@post), loading: :lazy do %>
|
|
30
|
+
<p>Loading comments...</p>
|
|
31
|
+
<% end %>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Turbo Streams
|
|
35
|
+
|
|
36
|
+
```erb
|
|
37
|
+
<%# app/views/posts/create.turbo_stream.erb %>
|
|
38
|
+
<%= turbo_stream.prepend "posts" do %>
|
|
39
|
+
<%= render @post %>
|
|
40
|
+
<% end %>
|
|
41
|
+
|
|
42
|
+
<%= turbo_stream.update "flash" do %>
|
|
43
|
+
<div class="notice">Post created!</div>
|
|
44
|
+
<% end %>
|
|
45
|
+
|
|
46
|
+
<%# Actions: append, prepend, replace, update, remove, before, after %>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Broadcasts from Model
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
class Post < ApplicationRecord
|
|
53
|
+
after_create_commit { broadcast_prepend_to "posts" }
|
|
54
|
+
after_update_commit { broadcast_replace_to "posts" }
|
|
55
|
+
after_destroy_commit { broadcast_remove_to "posts" }
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Stimulus
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// app/javascript/controllers/toggle_controller.js
|
|
63
|
+
import { Controller } from "@hotwired/stimulus"
|
|
64
|
+
|
|
65
|
+
export default class extends Controller {
|
|
66
|
+
static targets = ["content"]
|
|
67
|
+
|
|
68
|
+
toggle() {
|
|
69
|
+
this.contentTarget.classList.toggle("hidden")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
```erb
|
|
75
|
+
<div data-controller="toggle">
|
|
76
|
+
<button data-action="click->toggle#toggle">Toggle</button>
|
|
77
|
+
<div data-toggle-target="content">
|
|
78
|
+
Content here
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Best Practices
|
|
84
|
+
|
|
85
|
+
1. Start with Turbo Drive (enabled by default)
|
|
86
|
+
2. Use Turbo Frames for in-page updates
|
|
87
|
+
3. Use Turbo Streams for multi-element updates
|
|
88
|
+
4. Use Stimulus only when HTML-over-the-wire isn't enough
|
|
89
|
+
5. Keep Stimulus controllers small and focused
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-jobs
|
|
3
|
+
description: Active Job patterns, Sidekiq, background processing
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Background Jobs
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
| Pattern | Example |
|
|
12
|
+
|---------|---------|
|
|
13
|
+
| **Generate** | `rails g job ProcessOrder` |
|
|
14
|
+
| **Enqueue** | `ProcessOrderJob.perform_later(order)` |
|
|
15
|
+
| **Enqueue later** | `ProcessOrderJob.set(wait: 5.minutes).perform_later(order)` |
|
|
16
|
+
| **Enqueue at** | `ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order)` |
|
|
17
|
+
|
|
18
|
+
## Job Structure
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class ProcessOrderJob < ApplicationJob
|
|
22
|
+
queue_as :default
|
|
23
|
+
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
|
|
24
|
+
discard_on ActiveJob::DeserializationError
|
|
25
|
+
|
|
26
|
+
def perform(order)
|
|
27
|
+
order.process!
|
|
28
|
+
OrderMailer.confirmation(order).deliver_later
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Best Practices
|
|
34
|
+
|
|
35
|
+
1. Keep jobs idempotent
|
|
36
|
+
2. Pass IDs instead of full objects when possible
|
|
37
|
+
3. Use appropriate queues for different priorities
|
|
38
|
+
4. Handle retries and failures gracefully
|
|
39
|
+
5. Monitor queue depths in production
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-models
|
|
3
|
+
description: ActiveRecord patterns, migrations, validations, callbacks, associations
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Models (ActiveRecord)
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
| Pattern | Example |
|
|
12
|
+
|---------|---------|
|
|
13
|
+
| **Generate model** | `rails g model User name:string email:string` |
|
|
14
|
+
| **Migration** | `rails g migration AddAgeToUsers age:integer` |
|
|
15
|
+
| **Validation** | `validates :email, presence: true, uniqueness: true` |
|
|
16
|
+
| **Association** | `has_many :posts, dependent: :destroy` |
|
|
17
|
+
| **Scope** | `scope :active, -> { where(active: true) }` |
|
|
18
|
+
|
|
19
|
+
## Model Structure
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class User < ApplicationRecord
|
|
23
|
+
# Constants
|
|
24
|
+
ROLES = %w[admin editor viewer].freeze
|
|
25
|
+
|
|
26
|
+
# Associations
|
|
27
|
+
has_many :posts, dependent: :destroy
|
|
28
|
+
belongs_to :organization, optional: true
|
|
29
|
+
|
|
30
|
+
# Validations
|
|
31
|
+
validates :email, presence: true, uniqueness: { case_sensitive: false }
|
|
32
|
+
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
|
|
33
|
+
validates :role, inclusion: { in: ROLES }
|
|
34
|
+
|
|
35
|
+
# Callbacks
|
|
36
|
+
before_save :normalize_email
|
|
37
|
+
|
|
38
|
+
# Scopes
|
|
39
|
+
scope :active, -> { where(active: true) }
|
|
40
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
41
|
+
|
|
42
|
+
# Enums
|
|
43
|
+
enum :status, { pending: 0, active: 1, archived: 2 }
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def normalize_email
|
|
48
|
+
self.email = email.downcase.strip
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Migrations
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
class CreateUsers < ActiveRecord::Migration[7.0]
|
|
57
|
+
def change
|
|
58
|
+
create_table :users do |t|
|
|
59
|
+
t.string :name, null: false
|
|
60
|
+
t.string :email, null: false
|
|
61
|
+
t.boolean :active, default: true
|
|
62
|
+
t.references :organization, foreign_key: true
|
|
63
|
+
t.timestamps
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
add_index :users, :email, unique: true
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Associations
|
|
72
|
+
|
|
73
|
+
- `has_many :items, dependent: :destroy`
|
|
74
|
+
- `belongs_to :parent, optional: true`
|
|
75
|
+
- `has_many :tags, through: :taggings`
|
|
76
|
+
- `has_one :profile, dependent: :destroy`
|
|
77
|
+
- `has_many :comments, as: :commentable` (polymorphic)
|
|
78
|
+
|
|
79
|
+
## Validations
|
|
80
|
+
|
|
81
|
+
- `validates :field, presence: true`
|
|
82
|
+
- `validates :email, uniqueness: { case_sensitive: false }`
|
|
83
|
+
- `validates :age, numericality: { greater_than: 0 }`
|
|
84
|
+
- `validates :field, length: { minimum: 2, maximum: 100 }`
|
|
85
|
+
- `validates :field, format: { with: /\Apattern\z/ }`
|
|
86
|
+
- `validate :custom_validation_method`
|
|
87
|
+
|
|
88
|
+
## Queries
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
User.where(active: true)
|
|
92
|
+
User.where.not(role: "guest")
|
|
93
|
+
User.joins(:posts).where(posts: { published: true })
|
|
94
|
+
User.includes(:posts).order(created_at: :desc)
|
|
95
|
+
User.pluck(:id, :name)
|
|
96
|
+
User.group(:role).count
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Best Practices
|
|
100
|
+
|
|
101
|
+
1. Add database-level constraints (NOT NULL, unique indexes, foreign keys)
|
|
102
|
+
2. Use `includes` to avoid N+1 queries
|
|
103
|
+
3. Keep callbacks simple, use service objects for complex logic
|
|
104
|
+
4. Use scopes for reusable query fragments
|
|
105
|
+
5. Use concerns to share behavior across models
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rails-views
|
|
3
|
+
description: ERB templates, layouts, partials, forms, and view helpers
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rails Views
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
| Pattern | Example |
|
|
12
|
+
|---------|---------|
|
|
13
|
+
| **Output** | `<%= @post.title %>` |
|
|
14
|
+
| **Logic** | `<% if condition %>` |
|
|
15
|
+
| **Partial** | `<%= render "shared/header" %>` |
|
|
16
|
+
| **Collection** | `<%= render @posts %>` |
|
|
17
|
+
| **Form** | `<%= form_with model: @post do \|f\| %>` |
|
|
18
|
+
| **Link** | `<%= link_to "Home", root_path %>` |
|
|
19
|
+
|
|
20
|
+
## Layouts
|
|
21
|
+
|
|
22
|
+
```erb
|
|
23
|
+
<!DOCTYPE html>
|
|
24
|
+
<html>
|
|
25
|
+
<head>
|
|
26
|
+
<title><%= content_for?(:title) ? yield(:title) : "App" %></title>
|
|
27
|
+
<%= csrf_meta_tags %>
|
|
28
|
+
<%= stylesheet_link_tag "application" %>
|
|
29
|
+
<%= javascript_importmap_tags %>
|
|
30
|
+
</head>
|
|
31
|
+
<body>
|
|
32
|
+
<% flash.each do |type, message| %>
|
|
33
|
+
<div class="flash flash-<%= type %>"><%= message %></div>
|
|
34
|
+
<% end %>
|
|
35
|
+
<%= yield %>
|
|
36
|
+
</body>
|
|
37
|
+
</html>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Partials
|
|
41
|
+
|
|
42
|
+
```erb
|
|
43
|
+
<%# Render a partial %>
|
|
44
|
+
<%= render "post", post: @post %>
|
|
45
|
+
|
|
46
|
+
<%# Render a collection %>
|
|
47
|
+
<%= render partial: "post", collection: @posts %>
|
|
48
|
+
|
|
49
|
+
<%# Shorthand for collection %>
|
|
50
|
+
<%= render @posts %>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Forms
|
|
54
|
+
|
|
55
|
+
```erb
|
|
56
|
+
<%= form_with model: @post do |f| %>
|
|
57
|
+
<div>
|
|
58
|
+
<%= f.label :title %>
|
|
59
|
+
<%= f.text_field :title %>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div>
|
|
63
|
+
<%= f.label :body %>
|
|
64
|
+
<%= f.text_area :body %>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div>
|
|
68
|
+
<%= f.label :category_id %>
|
|
69
|
+
<%= f.collection_select :category_id, Category.all, :id, :name, prompt: "Select" %>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<%= f.submit %>
|
|
73
|
+
<% end %>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Helpers
|
|
77
|
+
|
|
78
|
+
```ruby
|
|
79
|
+
module ApplicationHelper
|
|
80
|
+
def page_title(title)
|
|
81
|
+
content_for(:title) { title }
|
|
82
|
+
content_tag(:h1, title)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def active_class(path)
|
|
86
|
+
current_page?(path) ? "active" : ""
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Turbo Frames
|
|
92
|
+
|
|
93
|
+
```erb
|
|
94
|
+
<%= turbo_frame_tag "post_#{@post.id}" do %>
|
|
95
|
+
<%= render @post %>
|
|
96
|
+
<% end %>
|
|
97
|
+
|
|
98
|
+
<%= turbo_frame_tag "post", src: post_path(@post), loading: :lazy do %>
|
|
99
|
+
Loading...
|
|
100
|
+
<% end %>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Best Practices
|
|
104
|
+
|
|
105
|
+
1. Keep logic out of views - use helpers or presenters
|
|
106
|
+
2. Use partials for reusable components
|
|
107
|
+
3. Always escape user input (ERB does this by default)
|
|
108
|
+
4. Use `content_for` for flexible layouts
|
|
109
|
+
5. Use Turbo Frames for partial page updates
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rspec-testing
|
|
3
|
+
description: RSpec testing patterns for models, controllers, requests, and system tests
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# RSpec Testing
|
|
8
|
+
|
|
9
|
+
## Quick Reference
|
|
10
|
+
|
|
11
|
+
| Pattern | Example |
|
|
12
|
+
|---------|---------|
|
|
13
|
+
| **Run all** | `bundle exec rspec` |
|
|
14
|
+
| **Run file** | `bundle exec rspec spec/models/user_spec.rb` |
|
|
15
|
+
| **Run line** | `bundle exec rspec spec/models/user_spec.rb:15` |
|
|
16
|
+
| **Run tag** | `bundle exec rspec --tag focus` |
|
|
17
|
+
|
|
18
|
+
## Model Specs
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
RSpec.describe User, type: :model do
|
|
22
|
+
describe "validations" do
|
|
23
|
+
it { is_expected.to validate_presence_of(:email) }
|
|
24
|
+
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe "associations" do
|
|
28
|
+
it { is_expected.to have_many(:posts).dependent(:destroy) }
|
|
29
|
+
it { is_expected.to belong_to(:organization).optional }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "#full_name" do
|
|
33
|
+
it "returns first and last name" do
|
|
34
|
+
user = build(:user, first_name: "Jane", last_name: "Doe")
|
|
35
|
+
expect(user.full_name).to eq("Jane Doe")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Request Specs
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
RSpec.describe "Posts", type: :request do
|
|
45
|
+
describe "GET /posts" do
|
|
46
|
+
it "returns a successful response" do
|
|
47
|
+
get posts_path
|
|
48
|
+
expect(response).to have_http_status(:ok)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe "POST /posts" do
|
|
53
|
+
let(:valid_params) { { post: { title: "Test", body: "Content" } } }
|
|
54
|
+
|
|
55
|
+
it "creates a post" do
|
|
56
|
+
expect {
|
|
57
|
+
post posts_path, params: valid_params
|
|
58
|
+
}.to change(Post, :count).by(1)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## System Specs
|
|
65
|
+
|
|
66
|
+
```ruby
|
|
67
|
+
RSpec.describe "Managing posts", type: :system do
|
|
68
|
+
before { driven_by(:rack_test) }
|
|
69
|
+
|
|
70
|
+
it "allows creating a new post" do
|
|
71
|
+
visit new_post_path
|
|
72
|
+
fill_in "Title", with: "My Post"
|
|
73
|
+
fill_in "Body", with: "Content"
|
|
74
|
+
click_on "Create Post"
|
|
75
|
+
|
|
76
|
+
expect(page).to have_content("Post created")
|
|
77
|
+
expect(page).to have_content("My Post")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Factories (FactoryBot)
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
FactoryBot.define do
|
|
86
|
+
factory :user do
|
|
87
|
+
name { "John Doe" }
|
|
88
|
+
email { Faker::Internet.email }
|
|
89
|
+
role { "viewer" }
|
|
90
|
+
|
|
91
|
+
trait :admin do
|
|
92
|
+
role { "admin" }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Best Practices
|
|
99
|
+
|
|
100
|
+
1. Test behavior, not implementation
|
|
101
|
+
2. Use `let` and `before` for setup, keep `it` blocks focused
|
|
102
|
+
3. Use factories instead of fixtures
|
|
103
|
+
4. Write request specs over controller specs
|
|
104
|
+
5. Use `have_http_status` for response assertions
|
|
105
|
+
6. Keep specs fast - minimize database interactions
|
data/lib/rails_skills.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails_skills/version"
|
|
4
|
+
require "rails_skills/railtie" if defined?(Rails::Railtie)
|
|
5
|
+
|
|
6
|
+
module RailsSkills
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
SKILLS_DIR = "skills"
|
|
10
|
+
CLAUDE_DIR = ".claude"
|
|
11
|
+
CODEX_DIR = ".codex"
|
|
12
|
+
end
|