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.
Files changed (26) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +99 -0
  4. data/Rakefile +5 -0
  5. data/lib/generators/rails_skills/commands_library/quality.md +16 -0
  6. data/lib/generators/rails_skills/install/install_generator.rb +211 -0
  7. data/lib/generators/rails_skills/install/templates/agents/api-dev.md.tt +28 -0
  8. data/lib/generators/rails_skills/install/templates/agents/fullstack-dev.md.tt +28 -0
  9. data/lib/generators/rails_skills/install/templates/agents/rails-developer.md.tt +22 -0
  10. data/lib/generators/rails_skills/install/templates/settings.local.json.tt +11 -0
  11. data/lib/generators/rails_skills/rules_library/code-style.md +28 -0
  12. data/lib/generators/rails_skills/rules_library/security.md +33 -0
  13. data/lib/generators/rails_skills/rules_library/testing.md +27 -0
  14. data/lib/generators/rails_skills/skill/skill_generator.rb +46 -0
  15. data/lib/generators/rails_skills/skill/templates/SKILL.md.tt +29 -0
  16. data/lib/generators/rails_skills/skills_library/rails-api-controllers/SKILL.md +106 -0
  17. data/lib/generators/rails_skills/skills_library/rails-controllers/SKILL.md +125 -0
  18. data/lib/generators/rails_skills/skills_library/rails-hotwire/SKILL.md +89 -0
  19. data/lib/generators/rails_skills/skills_library/rails-jobs/SKILL.md +39 -0
  20. data/lib/generators/rails_skills/skills_library/rails-models/SKILL.md +105 -0
  21. data/lib/generators/rails_skills/skills_library/rails-views/SKILL.md +109 -0
  22. data/lib/generators/rails_skills/skills_library/rspec-testing/SKILL.md +105 -0
  23. data/lib/rails_skills/railtie.rb +10 -0
  24. data/lib/rails_skills/version.rb +5 -0
  25. data/lib/rails_skills.rb +12 -0
  26. 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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSkills
4
+ class Railtie < ::Rails::Railtie
5
+ generators do
6
+ require "generators/rails_skills/install/install_generator"
7
+ require "generators/rails_skills/skill/skill_generator"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsSkills
4
+ VERSION = "0.1.0"
5
+ end
@@ -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