rails-developer

安装量: 45
排名: #16523

安装

npx skills add https://github.com/dengineproblem/agents-monorepo --skill rails-developer

Ruby on Rails Developer Expert in Ruby on Rails development with focus on Rails 8, Hotwire, and the Solid Trifecta stack. Core Principles stack_philosophy : framework : "Rails 8" database : "SQLite3 (production-ready)" background_jobs : "SolidQueue" websockets : "SolidCable" caching : "SolidCache" frontend : "Hotwire (Turbo + Stimulus)" styling : "Tailwind CSS" code_conventions : naming : files : "snake_case" methods : "snake_case" classes : "CamelCase" constants : "SCREAMING_SNAKE_CASE" structure : - "Follow Rails conventions" - "RESTful routing" - "Thin controllers, fat models" - "Service objects for complex logic" Rails 8 Features Authentication

Generate built-in authentication

rails g authentication

app/models/user.rb

class User < ApplicationRecord has_secure_password normalizes :email , with : -

( email ) { email . strip . downcase } validates :email , presence : true , uniqueness : true , format : { with : URI :: MailTo :: EMAIL_REGEXP } end

app/controllers/sessions_controller.rb

class SessionsController < ApplicationController def create user = User . find_by ( email : params [ :email ] ) if user &. authenticate ( params [ :password ] ) session [ :user_id ] = user . id redirect_to root_path , notice : "Logged in!" else flash . now [ :alert ] = "Invalid email or password" render :new , status : :unprocessable_entity end end def destroy session [ :user_id ] = nil redirect_to root_path , notice : "Logged out!" end end Solid Trifecta

config/queue.yml - SolidQueue configuration

production : workers : - queues : "*" threads : 5 polling_interval : 0.1 dispatchers : - polling_interval : 1 batch_size : 500

app/jobs/process_order_job.rb

class ProcessOrderJob < ApplicationJob queue_as :default retry_on StandardError , wait : :polynomially_longer , attempts : 5 def perform ( order_id ) order = Order . find ( order_id ) OrderProcessor . new ( order ) . process ! end end

Using SolidCable for ActionCable

config/cable.yml

production : adapter : solid_cable polling_interval : 0.1

Using SolidCache

config/cache.yml

production : store : solid_cache size : 256. megabytes ActiveRecord Patterns Models & Validations

app/models/product.rb

class Product < ApplicationRecord belongs_to :category has_many :order_items , dependent : :restrict_with_error has_many :orders , through : :order_items has_one_attached :image has_rich_text :description enum :status , { draft : 0 , published : 1 , archived : 2 } validates :name , presence : true , length : { maximum : 255 } validates :price , presence : true , numericality : { greater_than : 0 } validates :sku , presence : true , uniqueness : true scope :available , -

{ published . where ( "stock > 0" ) } scope :featured , -

{ where ( featured : true ) . order ( created_at : :desc ) } before_validation :generate_sku , on : :create after_commit :update_search_index , on : [ :create , :update ] private def generate_sku self . sku ||= "SKU-

{

SecureRandom . hex ( 4 ) . upcase } " end def update_search_index SearchIndexJob . perform_later ( self ) end end Query Interface

Efficient queries

class ProductQuery def initialize ( relation = Product . all ) @relation = relation end def search ( term ) return self if term . blank ? @relation = @relation . where ( "name ILIKE :term OR description ILIKE :term" , term : "%

{

term } %" ) self end def by_category ( category_id ) return self if category_id . blank ? @relation = @relation . where ( category_id : category_id ) self end def price_range ( min : , max : ) @relation = @relation . where ( price : min .. max ) if min && max self end def results @relation end end

Usage

products

ProductQuery . new . search ( params [ :q ] ) . by_category ( params [ :category_id ] ) . price_range ( min : 10 , max : 100 ) . results . includes ( :category , image_attachment : :blob ) . page ( params [ :page ] ) Migrations

db/migrate/20241201000000_create_orders.rb

class CreateOrders < ActiveRecord :: Migration [ 8.0 ] def change create_table :orders do | t | t . references :user , null : false , foreign_key : true t . string :number , null : false , index : { unique : true } t . integer :status , null : false , default : 0 t . decimal :total , precision : 10 , scale : 2 , null : false , default : 0 t . jsonb :metadata , null : false , default : { } t . datetime :completed_at t . timestamps end add_index :orders , :status add_index :orders , :completed_at add_index :orders , [ :user_id , :status ] end end Hotwire Turbo Frames

<%= turbo_frame_tag "products" do %> < div class = " grid grid-cols-3 gap-4 "

% @products . each do | product | % <%= render product %> <% end %> </ div

%= paginate @products % <% end %>

<%= turbo_frame_tag dom_id ( product ) do %> < div class = " card " data-controller = " product "

< h3

<%= product . name %> </ h3

< p

<%= number_to_currency product . price %> </ p

<%= link_to "Edit" , edit_product_path ( product ) , data : { turbo_frame : "modal" } %> </ div

<% end %> Turbo Streams

app/controllers/comments_controller.rb

class CommentsController < ApplicationController def create @comment = @post . comments . build ( comment_params ) @comment . user = current_user if @comment . save respond_to do | format | format . turbo_stream format . html { redirect_to @post } end else render :new , status : :unprocessable_entity end end end

app/views/comments/create.turbo_stream.erb

%= turbo_stream . prepend "comments" , @comment % <%= turbo_stream.update "new_comment" do %> < %= render "form" , comment : Comment . new %> <% end %> < %= turbo_stream . update "comments_count" , @post . comments . count %

Stimulus Controllers // app/javascript/controllers/form_controller.js import { Controller } from "@hotwired/stimulus" export default class extends Controller { static targets = [ "input" , "submit" , "output" ] static values = { url : String , debounce : { type : Number , default : 300 } } connect ( ) { this . timeout = null } search ( ) { clearTimeout ( this . timeout ) this . timeout = setTimeout ( ( ) => { this . performSearch ( ) } , this . debounceValue ) } async performSearch ( ) { const query = this . inputTarget . value if ( query . length < 2 ) return this . submitTarget . disabled = true try { const response = await fetch ( ${ this . urlValue } ?q= ${ encodeURIComponent ( query ) } ) const html = await response . text ( ) this . outputTarget . innerHTML = html } finally { this . submitTarget . disabled = false } } } Testing with RSpec

spec/models/product_spec.rb

RSpec . describe Product , type : :model do describe "validations" do subject { build ( :product ) } it { should validate_presence_of ( :name ) } it { should validate_presence_of ( :price ) } it { should validate_uniqueness_of ( :sku ) } it { should validate_numericality_of ( :price ) . is_greater_than ( 0 ) } end describe "associations" do it { should belong_to ( :category ) } it { should have_many ( :order_items ) } end describe "scopes" do describe ".available" do let ! ( :available ) { create ( :product , status : :published , stock : 10 ) } let ! ( :out_of_stock ) { create ( :product , status : :published , stock : 0 ) } let ! ( :draft ) { create ( :product , status : :draft , stock : 10 ) } it "returns only published products with stock" do expect ( Product . available ) . to contain_exactly ( available ) end end end end

spec/requests/products_spec.rb

RSpec . describe "Products" , type : :request do describe "GET /products" do it "returns successful response" do get products_path expect ( response ) . to have_http_status ( :success ) end end describe "POST /products" do let ( :valid_params ) { { product : attributes_for ( :product ) } } context "with valid params" do it "creates a new product" do expect { post products_path , params : valid_params } . to change ( Product , :count ) . by ( 1 ) end end end end

spec/system/products_spec.rb

RSpec . describe "Product management" , type : :system do before { driven_by ( :selenium_chrome_headless ) } it "allows creating a new product" do visit new_product_path fill_in "Name" , with : "Test Product" fill_in "Price" , with : "99.99" click_button "Create Product" expect ( page ) . to have_content ( "Product was successfully created" ) expect ( page ) . to have_content ( "Test Product" ) end end Performance Optimization Caching

Fragment caching with Russian Doll strategy

app/views/products/_product.html.erb

< % cache product do %>

< % cache [ product , "details" ] do %>

< %= product . name %>

< p > < %= product . description %>

< % end %> <% cache [product.category, "category"] do %> < span class = "category" > < %= product . category . name %> < % end %>

< % end %

Low-level caching

class Product < ApplicationRecord def expensive_calculation Rails . cache . fetch ( [ cache_key_with_version , "calculation" ] , expires_in : 1. hour ) do

Complex computation

perform_expensive_operation end end end

Counter caching

class Comment < ApplicationRecord belongs_to :post , counter_cache : true end N+1 Prevention

app/controllers/posts_controller.rb

class PostsController < ApplicationController def index @posts = Post . includes ( :author , :comments , :tags ) . with_attached_image . order ( created_at : :desc ) . page ( params [ :page ] ) end end

Using strict loading in development

config/environments/development.rb

config . active_record . strict_loading_by_default = true Database Optimization

Add proper indexes

class AddIndexesToProducts < ActiveRecord :: Migration [ 8.0 ] def change add_index :products , :category_id add_index :products , [ :status , :created_at ] add_index :products , :price

Partial index

add_index :products , :featured , where : "featured = true"

GIN index for JSONB

add_index :products , :metadata , using : :gin end end

Bulk operations

Product . insert_all ( [ { name : "Product 1" , price : 10 } , { name : "Product 2" , price : 20 } ] ) Product . where ( status : :draft ) . update_all ( status : :archived ) Security

Strong parameters

class ProductsController < ApplicationController private def product_params params . require ( :product ) . permit ( :name , :price , :description , :category_id , :image , tags : [ ] ) end end

Authorization

class ApplicationController < ActionController :: Base before_action :authenticate_user! private def authorize_admin ! redirect_to root_path , alert : "Not authorized" unless current_user . admin ? end end

Content Security Policy

config/initializers/content_security_policy.rb

Rails . application . configure do config . content_security_policy do | policy | policy . default_src :self , :https policy . font_src :self , :https , :data policy . img_src :self , :https , :data policy . object_src :none policy . script_src :self , :https policy . style_src :self , :https , :unsafe_inline end end API Mode

app/controllers/api/v1/base_controller.rb

module Api module V1 class BaseController < ActionController :: API include ActionController :: HttpAuthentication :: Token :: ControllerMethods before_action :authenticate_api_user! private def authenticate_api_user ! authenticate_or_request_with_http_token do | token , options | @current_api_user = User . find_by ( api_token : token ) end end def current_api_user @current_api_user end end end end

app/controllers/api/v1/products_controller.rb

module Api module V1 class ProductsController < BaseController def index products = Product . available . page ( params [ :page ] ) . per ( 25 ) render json : { products : products . as_json ( only : [ :id , :name , :price ] ) , meta : { current_page : products . current_page , total_pages : products . total_pages , total_count : products . total_count } } end def show product = Product . find ( params [ :id ] ) render json : ProductSerializer . new ( product ) . as_json end end end end Лучшие практики Convention over Configuration — следуй Rails conventions Fat Model, Skinny Controller — логика в моделях и сервисах Hotwire first — минимум JavaScript, максимум Turbo Test everything — RSpec для моделей, запросов и системных тестов Cache strategically — Russian Doll caching для производительности Secure by default — strong parameters, CSP, аутентификация

返回排行榜