Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

A community-driven Ruby on Rails style guide

NotificationsYou must be signed in to change notification settings

rubocop/rails-style-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Role models are important.

 — Officer Alex J. Murphy / RoboCop

Tip
You can find a beautiful version of this guide with much improved navigation athttps://rails.rubystyle.guide.

The goal of this guide is to present a set of best practices and style prescriptions for Ruby on Rails development.It’s a complementary guide to the already existing community-drivenRuby coding style guide.

This Rails style guide recommends best practices so that real-world Rails programmers can write code that can be maintained by other real-world Rails programmers.A style guide that reflects real-world usage gets used, and a style guide that holds to an ideal that has been rejected by the people it is supposed to help risks not getting used at all - no matter how good it is.

The guide is separated into several sections of related rules.I’ve tried to add the rationale behind the rules (if it’s omitted I’ve assumed it’s pretty obvious).

I didn’t come up with all the rules out of nowhere - they are mostly based on my extensive career as a professional software engineer, feedback and suggestions from members of the Rails community and various highly regarded Rails programming resources.

Note
Some of the advice here is applicable only to recent versions of Rails.

You can generate a PDF copy of this guide usingAsciiDoctor PDF, and an HTML copywithAsciiDoctor using the following commands:

# Generates README.pdfasciidoctor-pdf -a allow-uri-read README.adoc# Generates README.htmlasciidoctor README.adoc
Tip

Install therouge gem to get nice syntax highlighting in the generated document.

gem install rouge

Translations of the guide are available in the following languages:

Tip
RuboCop, a static code analyzer (linter) and formatter, has arubocop-rails extension, based on this style guide.

Put custom initialization code inconfig/initializers.The code in initializers executes on application startup.

Keep initialization code for each gem in a separate file with the same name as the gem, for examplecarrierwave.rb,active_admin.rb, etc.

Adjust accordingly the settings for development, test and production environment (in the corresponding files underconfig/environments/)

Mark additional assets for precompilation (if any):

# config/environments/production.rb# Precompile additional assets (application.js, application.css,#and all non-JS/CSS are already added)config.assets.precompile +=%w(rails_admin/rails_admin.cssrails_admin/rails_admin.js)

Keep configuration that’s applicable to all environments in theconfig/application.rb file.

When upgrading to a newer Rails version, your application’s configuration setting will remain on the previous version. To take advantage of the latest recommended Rails practices, theconfig.load_defaults setting should match your Rails version.

# goodconfig.load_defaults6.1

Avoid creating additional environment configurations than the defaults ofdevelopment,test andproduction.If you need a production-like environment such as staging, use environment variables for configuration options.

Keep any additional configuration in YAML files under theconfig/ directory.

Since Rails 4.2 YAML configuration files can be easily loaded with the newconfig_for method:

Rails::Application.config_for(:yaml_file)

When you need to add more actions to a RESTful resource (do you really need them at all?) usemember andcollection routes.

# badget'subscriptions/:id/unsubscribe'resources:subscriptions# goodresources:subscriptionsdoget'unsubscribe',on::memberend# badget'photos/search'resources:photos# goodresources:photosdoget'search',on::collectionend

If you need to define multiplemember/collection routes use the alternative block syntax.

resources:subscriptionsdomemberdoget'unsubscribe'# more routesendendresources:photosdocollectiondoget'search'# more routesendend

Use nested routes to express better the relationship between Active Record models.

classPost <ApplicationRecordhas_many:commentsendclassComment <ApplicationRecordbelongs_to:postend# routes.rbresources:postsdoresources:commentsend

If you need to nest routes more than 1 level deep then use theshallow: true option.This will save user from long URLsposts/1/comments/5/versions/7/edit and you from long URL helpersedit_post_comment_version.

resources:posts,shallow:truedoresources:commentsdoresources:versionsendend

Use namespaced routes to group related actions.

namespace:admindo# Directs /admin/products/* to Admin::ProductsController# (app/controllers/admin/products_controller.rb)resources:productsend

Never use the legacy wild controller route.This route will make all actions in every controller accessible via GET requests.

# very badmatch':controller(/:action(/:id(.:format)))'

Don’t usematch to define any routes unless there is need to map multiple request types among[:get, :post, :patch, :put, :delete] to a single action using:via option.

Keep the controllers skinny - they should only retrieve data for the view layer and shouldn’t contain any business logic (all the business logic should naturally reside in the model).

Each controller action should (ideally) invoke only one method other than an initial find or new.

Minimize the number of instance variables passed between a controller and a view.

Controller actions specified in the option of Action Filter should be in lexical scope.The ActionFilter specified for an inherited action makes it difficult to understand the scope of its impact on that action.

# badclassUsersController <ApplicationControllerbefore_action:require_login,only::exportend# goodclassUsersController <ApplicationControllerbefore_action:require_login,only::exportdefexportendend

Prefer using a template over inline rendering.

# very badclassProductsController <ApplicationControllerdefindexrenderinline:"<% products.each do |p| %><p><%= p.name %></p><% end %>",type::erbendend# good## app/views/products/index.html.erb<%=renderpartial:'product',collection:products %>## app/views/products/_product.html.erb<p><%=product.name %></p><p><%= product.price %></p>## app/controllers/products_controller.rbclassProductsController <ApplicationControllerdefindexrender:indexendend

Preferrender plain: overrender text:.

# bad - sets MIME type to `text/html`...rendertext:'Ruby!'...# bad - requires explicit MIME type declaration...rendertext:'Ruby!',content_type:'text/plain'...# good - short and precise...renderplain:'Ruby!'...

Prefercorresponding symbols to numeric HTTP status codes.They are meaningful and do not look like "magic" numbers for less known HTTP status codes.

# bad...renderstatus:403...# good...renderstatus::forbidden...

Introduce non-Active Record model classes freely.

Name the models with meaningful (but short) names without abbreviations.

If you need objects that support ActiveRecord-like behavior (like validations) without the database functionality, useActiveModel::Model.

classMessageincludeActiveModel::Modelattr_accessor:name,:email,:content,:priorityvalidates:name,presence:truevalidates:email,format:{with:/\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i}validates:content,length:{maximum:500}end

Starting with Rails 6.1, you can also extend the attributes API from ActiveRecord usingActiveModel::Attributes.

classMessageincludeActiveModel::ModelincludeActiveModel::Attributesattribute:name,:stringattribute:email,:stringattribute:content,:stringattribute:priority,:integervalidates:name,presence:truevalidates:email,format:{with:/\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i}validates:content,length:{maximum:500}end

Unless they have some meaning in the business domain, don’t put methods in your model that just format your data (like code generating HTML).These methods are most likely going to be called from the view layer only, so their place is in helpers.Keep your models for business logic and data-persistence only.

Avoid altering Active Record defaults (table names, primary key, etc) unless you have a very good reason (like a database that’s not under your control).

# bad - don't do this if you can modify the schemaclassTransaction <ApplicationRecordself.table_name='order'  ...end

Avoid settingignored_columns. It may overwrite previous assignments and that is almost always a mistake. Prefer appending to the list instead.

classTransaction <ApplicationRecord# bad - it may overwrite previous assignmentsself.ignored_columns=%i[legacy]# good - the value is appended to the listself.ignored_columns +=%i[legacy]  ...end

Prefer using the hash syntax forenum. Array makes the database values implicit& any insertion/removal/rearrangement of values in the middle will most probablylead to broken code.

classTransaction <ApplicationRecord# bad - implicit values - ordering mattersenumtype:%i[creditdebit]# good - explicit values - ordering does not matterenumtype:{credit:0,debit:1}end

Group macro-style methods (has_many,validates, etc) in the beginning of the class definition.

classUser <ApplicationRecord# keep the default scope first (if any)default_scope{where(active:true)}# constants come up nextCOLORS=%w(redgreenblue)# afterwards we put attr related macrosattr_accessor:formatted_date_of_birthattr_accessible:login,:first_name,:last_name,:email,:password# Rails 4+ enums after attr macrosenumrole:{user:0,moderator:1,admin:2}# followed by association macrosbelongs_to:countryhas_many:authentications,dependent::destroy# and validation macrosvalidates:email,presence:truevalidates:username,presence:truevalidates:username,uniqueness:{case_sensitive:false}validates:username,format:{with:/\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/}validates:password,format:{with:/\A\S{8,128}\z/,allow_nil:true}# next we have callbacksbefore_save:cookbefore_save:update_username_lower# other macros (like devise's) should be placed after the callbacks  ...end

Preferhas_many :through tohas_and_belongs_to_many.Usinghas_many :through allows additional attributes and validations on the join model.

# not so good - using has_and_belongs_to_manyclassUser <ApplicationRecordhas_and_belongs_to_many:groupsendclassGroup <ApplicationRecordhas_and_belongs_to_many:usersend# preferred way - using has_many :throughclassUser <ApplicationRecordhas_many:membershipshas_many:groups,through::membershipsendclassMembership <ApplicationRecordbelongs_to:userbelongs_to:groupendclassGroup <ApplicationRecordhas_many:membershipshas_many:users,through::membershipsend

Preferself[:attribute] overread_attribute(:attribute).

# baddefamountread_attribute(:amount) *100end# gooddefamountself[:amount] *100end

Preferself[:attribute] = value overwrite_attribute(:attribute, value).

# baddefamountwrite_attribute(:amount,100)end# gooddefamountself[:amount]=100end
# badvalidates_presence_of:emailvalidates_length_of:email,maximum:100# goodvalidates:email,presence:true,length:{maximum:100}

When naming custom validation methods, adhere to the simple rules:

  • validate :method_name reads like a natural statement

  • the method name explains what it checks

  • the method is recognizable as a validation method by its name, not a predicate method

# goodvalidate:expiration_date_cannot_be_in_the_pastvalidate:discount_cannot_be_greater_than_total_valuevalidate:ensure_same_topic_is_chosen# also good - explicit prefixvalidate:validate_birthday_in_pastvalidate:validate_sufficient_quantityvalidate:must_have_owner_with_no_other_itemsvalidate:must_have_shipping_units# badvalidate:birthday_in_pastvalidate:owner_has_no_other_items

To make validations easy to read, don’t list multiple attributes per validation.

# badvalidates:email,:password,presence:truevalidates:email,length:{maximum:100}# goodvalidates:email,presence:true,length:{maximum:100}validates:password,presence:true

When a custom validation is used more than once or the validation is some regular expression mapping, create a custom validator file.

# badclassPersonvalidates:email,format:{with:/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i}end# goodclassEmailValidator <ActiveModel::EachValidatordefvalidate_each(record,attribute,value)record.errors[attribute] <<(options[:message] ||'is not a valid email')unlessvalue =~/\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/iendendclassPersonvalidates:email,email:trueend

Keep custom validators underapp/validators.

Consider extracting custom validators to a shared gem if you’re maintaining several related apps or the validators are generic enough.

Use named scopes freely.

classUser <ApplicationRecordscope:active,->{where(active:true)}scope:inactive,->{where(active:false)}scope:with_orders,->{joins(:orders).select('distinct(users.id)')}end

When a named scope defined with a lambda and parameters becomes too complicated, it is preferable to make a class method instead which serves the same purpose of the named scope and returns anActiveRecord::Relation object.Arguably you can define even simpler scopes like this.

classUser <ApplicationRecorddefself.with_ordersjoins(:orders).select('distinct(users.id)')endend

Order callback declarations in the order in which they will be executed.For reference, seeAvailable Callbacks.

# badclassPersonafter_commit:after_commit_callbackbefore_validation:before_validation_callbackend# goodclassPersonbefore_validation:before_validation_callbackafter_commit:after_commit_callbackend

Beware of the behavior of thefollowing methods.They do not run the model validations and could easily corrupt the model state.

# badArticle.first.decrement!(:view_count)DiscussionBoard.decrement_counter(:post_count,5)Article.first.increment!(:view_count)DiscussionBoard.increment_counter(:post_count,5)person.toggle:activeproduct.touchBilling.update_all("category = 'authorized', author = 'David'")user.update_attribute(:website,'example.com')user.update_columns(last_request_at:Time.current)Post.update_counters5,comment_count: -1,action_count:1# gooduser.update_attributes(website:'example.com')

Use user-friendly URLs.Show some descriptive attribute of the model in the URL rather than itsid.There is more than one way to achieve this.

This method is used by Rails for constructing a URL to the object.The default implementation returns theid of the record as a String.It could be overridden to include another human-readable attribute.

classPersondefto_param"#{id}#{name}".parameterizeendend

In order to convert this to a URL-friendly value,parameterize should be called on the string.Theid of the object needs to be at the beginning so that it can be found by thefind method of Active Record.

It allows creation of human-readable URLs by using some descriptive attribute of the model instead of itsid.

classPersonextendFriendlyIdfriendly_id:name,use::sluggedend

Check thegem documentation for more information about its usage.

Usefind_each to iterate over a collection of AR objects.Looping through a collection of records from the database (using theall method, for example) is very inefficient since it will try to instantiate all the objects at once.In that case, batch processing methods allow you to work with the records in batches, thereby greatly reducing memory consumption.

# badPerson.all.eachdo |person|person.do_awesome_stuffendPerson.where('age > 21').eachdo |person|person.party_all_night!end# goodPerson.find_eachdo |person|person.do_awesome_stuffendPerson.where('age > 21').find_eachdo |person|person.party_all_night!end

SinceRails creates callbacks for dependent associations, always callbefore_destroy callbacks that perform validation withprepend: true.

# bad (roles will be deleted automatically even if super_admin? is true)has_many:roles,dependent::destroybefore_destroy:ensure_deletabledefensure_deletableraise"Cannot delete super admin."ifsuper_admin?end# goodhas_many:roles,dependent::destroybefore_destroy:ensure_deletable,prepend:truedefensure_deletableraise"Cannot delete super admin."ifsuper_admin?end

Define thedependent option to thehas_many andhas_one associations.

# badclassPost <ApplicationRecordhas_many:commentsend# goodclassPost <ApplicationRecordhas_many:comments,dependent::destroyend

When persisting AR objects always use the exception raising bang! method or handle the method return value.This applies tocreate,save,update,destroy,first_or_create andfind_or_create_by.

# baduser.create(name:'Bruce')# baduser.save# gooduser.create!(name:'Bruce')# orbruce=user.create(name:'Bruce')ifbruce.persisted?  ...else  ...end# gooduser.save!# orifuser.save  ...else  ...end

Avoid string interpolation in queries, as it will make your code susceptible to SQL injection attacks.

# bad - param will be interpolated unescapedClient.where("orders_count =#{params[:orders]}")# good - param will be properly escapedClient.where('orders_count = ?',params[:orders])

Consider using named placeholders instead of positional placeholders when you have more than 1 placeholder in your query.

# okishClient.where('orders_count >= ? AND country_code = ?',params[:min_orders_count],params[:country_code])# goodClient.where('orders_count >= :min_orders_count AND country_code = :country_code',min_orders_count:params[:min_orders_count],country_code:params[:country_code])

Preferfind overwhere.take!,find_by!, andfind_by_id! when you need to retrieve a single record by primary key id and raiseActiveRecord::RecordNotFound when the record is not found.

# badUser.where(id:id).take!# badUser.find_by_id!(id)# badUser.find_by!(id:id)# goodUser.find(id)

Preferfind_by overwhere.take andfind_by_attribute when you need to retrieve a single record by one or more attributes and returnnil when the record is not found.

# badUser.where(email:email).takeUser.where(first_name:'Bruce',last_name:'Wayne').take# badUser.find_by_email(email)User.find_by_first_name_and_last_name('Bruce','Wayne')# goodUser.find_by(email:email)User.find_by(first_name:'Bruce',last_name:'Wayne')

Prefer passing conditions towhere andwhere.not as a hash over using fragments of SQL.

# badUser.where("name = ?",name)# goodUser.where(name:name)# badUser.where("id != ?",id)# goodUser.where.not(id:id)

If you’re using Rails 6.1 or higher, usewhere.missing to find missing relationship records.

# badPost.left_joins(:author).where(authors:{id:nil})# goodPost.where.missing(:author)

Don’t use theid column for ordering.The sequence of ids is not guaranteed to be in any particular order, despite often (incidentally) being chronological.Use a timestamp column to order chronologically.As a bonus the intent is clearer.

# badscope:chronological,->{order(id::asc)}# goodscope:chronological,->{order(created_at::asc)}

Usepluck to select a single value from multiple records.

# badUser.all.map(&:name)# badUser.all.map{ |user|user[:name]}# goodUser.pluck(:name)

Usepick to select a single value from a single record.

# badUser.pluck(:name).first# badUser.first.name# goodUser.pick(:name)

Preferids overpluck(:id).

# badUser.pluck(:id)# goodUser.ids

When specifying an explicit query in a method such asfind_by_sql, use heredocs withsquish.This allows you to legibly format the SQL with line breaks and indentations, while supporting syntax highlighting in many tools (including GitHub, Atom, and RubyMine).

User.find_by_sql(<<-SQL.squish)  SELECT    users.id, accounts.plan  FROM    users  INNER JOIN    accounts  ON    accounts.user_id = users.id  # further complexities...SQL

String#squish removes the indentation and newline characters so that your server log shows a fluid string of SQL rather than something like this:

SELECT\n    users.id, accounts.plan\n  FROM\n    users\n  INNER JOIN\n    accounts\n  ON\n    accounts.user_id = users.id

When querying Active Record collections, prefersize (selects between count/length behavior based on whether collection is already loaded) orlength (always loads the whole collection and counts the array elements) overcount (always does a database query for the count).

# badUser.count# goodUser.all.size# good - if you really need to load all users into memoryUser.all.length

Use ranges instead of defining comparative conditions using a template for scalar values.

# badUser.where("created_at >= ?",30.days.ago).where("created_at <= ?",7.days.ago)User.where("created_at >= ? AND created_at <= ?",30.days.ago,7.days.ago)User.where("created_at >= :start AND created_at <= :end",start:30.days.ago,end:7.days.ago)# goodUser.where(created_at:30.days.ago..7.days.ago)# badUser.where("created_at >= ?",7.days.ago)# goodUser.where(created_at:7.days.ago..)# note - ranges are inclusive or exclusive of their ending, not beginningUser.where(created_at:7.days.ago..)# produces >=User.where(created_at:7.days.ago...)# also produces >=User.where(created_at: ..7.days.ago)# inclusive: produces <=User.where(created_at: ...7.days.ago)# exclusive: produces <# okish - there is no range syntax that would denote exclusion at the beginning of the rangeCustomer.where("purchases_count > :min AND purchases_count <= :max",min:0,max:5)
Note
Rails 6.0 or later is required for endless range Ruby 2.6 syntax, and Rails 6.0.3 for beginless range Ruby 2.7 syntax.

Avoid passing multiple attributes towhere.not. Rails logic in this case has changed in Rails 6.1 andwill now yield results matching either of those conditions,e.g.where.not(status: 'active', plan: 'basic') would return records with active status when the plan is business.

# badUser.where.not(status:'active',plan:'basic')# goodUser.where.not('status = ? AND plan = ?','active','basic')

Usingall as a receiver is redundant. The result won’t change withoutall, so it should be removed.

# badUser.all.find(id)User.all.order(:created_at)users.all.where(id:ids)user.articles.all.order(:created_at)# goodUser.find(id)User.order(:created_at)users.where(id:ids)user.articles.order(:created_at)
Note
When the receiver forall is an association, there are methods whose behavior changes by omittingall.

The following methods behave differently withoutall:

So, when considering removingall from the receiver of these methods, it is recommended to refer to the documentation to understand how the behavior changes.

Keep theschema.rb (orstructure.sql) under version control.

Userake db:schema:load instead ofrake db:migrate to initialize an empty database.

Enforce default values in the migrations themselves instead of in the application layer.

# bad - application enforced default valueclassProduct <ApplicationRecorddefamountself[:amount] ||0endend# good - database enforcedclassAddDefaultAmountToProducts <ActiveRecord::Migrationdefchangechange_column_default:products,:amount,0endend

While enforcing table defaults only in Rails is suggested by many Rails developers, it’s an extremely brittle approach that leaves your data vulnerable to many application bugs.And you’ll have to consider the fact that most non-trivial apps share a database with other applications, so imposing data integrity from the Rails app is impossible.

With SQL databases, if a boolean column is not given a default value, it will have three possible values:true,false andNULL.Boolean operatorswork in unexpected ways withNULL.

For example in SQL queries,true AND NULL isNULL (not false),true AND NULL OR false isNULL (not false). This can make SQL queries return unexpected results.

To avoid such situations, boolean columns should always have a default value and aNOT NULL constraint.

# bad - boolean without a default valueadd_column:users,:active,:boolean# good - boolean with a default value (`false` or `true`) and with restricted `NULL`add_column:users,:active,:boolean,default:true,null:falseadd_column:users,:admin,:boolean,default:false,null:false

Enforce foreign-key constraints. As of Rails 4.2, Active Record supports foreign key constraints natively.

# bad - does not add foreign keyscreate_table:commentdo |t|t.references:articlet.belongs_to:usert.integer:category_idend# goodcreate_table:commentdo |t|t.references:article,foreign_key:truet.belongs_to:user,foreign_key:truet.references:category,foreign_key:{to_table::comment_categories}end

When writing constructive migrations (adding tables or columns), use thechange method instead ofup anddown methods.

# the old wayclassAddNameToPeople <ActiveRecord::Migrationdefupadd_column:people,:name,:stringenddefdownremove_column:people,:nameendend# the new preferred wayclassAddNameToPeople <ActiveRecord::Migrationdefchangeadd_column:people,:name,:stringendend

If you have to use models in migrations, make sure you define them so that you don’t end up with broken migrations in the future.

# db/migrate/<migration_file_name>.rb# frozen_string_literal: true# badclassModifyDefaultStatusForProducts <ActiveRecord::Migrationdefchangeold_status='pending_manual_approval'new_status='pending_approval'reversibledo |dir|dir.updoProduct.where(status:old_status).update_all(status:new_status)change_column:products,:status,:string,default:new_statusenddir.downdoProduct.where(status:new_status).update_all(status:old_status)change_column:products,:status,:string,default:old_statusendendendend# good# Define `table_name` in a custom named class to make sure that you run on the# same table you had during the creation of the migration.# In future if you override the `Product` class and change the `table_name`,# it won't break the migration or cause serious data corruption.classMigrationProduct <ActiveRecord::Baseself.table_name=:productsendclassModifyDefaultStatusForProducts <ActiveRecord::Migrationdefchangeold_status='pending_manual_approval'new_status='pending_approval'reversibledo |dir|dir.updoMigrationProduct.where(status:old_status).update_all(status:new_status)change_column:products,:status,:string,default:new_statusenddir.downdoMigrationProduct.where(status:new_status).update_all(status:old_status)change_column:products,:status,:string,default:old_statusendendendend

Name your foreign keys explicitly instead of relying on Rails auto-generated FK names. (https://guides.rubyonrails.org/active_record_migrations.html#foreign-keys)

# badclassAddFkArticlesToAuthors <ActiveRecord::Migrationdefchangeadd_foreign_key:articles,:authorsendend# goodclassAddFkArticlesToAuthors <ActiveRecord::Migrationdefchangeadd_foreign_key:articles,:authors,name::articles_author_id_fkendend

Don’t use non-reversible migration commands in thechange method.Reversible migration commands are listed below.ActiveRecord::Migration::CommandRecorder

# badclassDropUsers <ActiveRecord::Migrationdefchangedrop_table:usersendend# goodclassDropUsers <ActiveRecord::Migrationdefupdrop_table:usersenddefdowncreate_table:usersdo |t|t.string:nameendendend# good# In this case, block will be used by create_table in rollback# https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_tableclassDropUsers <ActiveRecord::Migrationdefchangedrop_table:usersdo |t|t.string:nameendendend

Never call the model layer directly from a view.

Avoid complex formatting in the views.A view helper is useful for simple cases, but if it’s more complex then consider using a decorator or presenter.

Mitigate code duplication by using partial templates and layouts.

Avoid using instance variables in partials, pass a local variable torender instead.The partial may be used in a different controller or action, where the variable can have a different name or even be absent.In these cases, an undefined instance variable will not raise an exception whereas a local variable will.

<!-- bad --><!-- app/views/courses/show.html.erb --><%=render'course_description'%><!-- app/views/courses/_course_description.html.erb --><%=@course.description%><!-- good --><!-- app/views/courses/show.html.erb --><%=render'course_description',course:@course%><!-- app/views/courses/_course_description.html.erb --><%=course.description%>

No strings or other locale specific settings should be used in the views, models and controllers.These texts should be moved to the locale files in theconfig/locales directory.

When the labels of an Active Record model need to be translated, use theactiverecord scope:

en:  activerecord:    models:      user: Member    attributes:      user:        name: 'Full name'

ThenUser.model_name.human will return "Member" andUser.human_attribute_name("name") will return "Full name".These translations of the attributes will be used as labels in the views.

Separate the texts used in the views from translations of Active Record attributes.Place the locale files for the models in a folderlocales/models and the texts used in the views in folderlocales/views.

When organization of the locale files is done with additional directories, these directories must be described in theapplication.rb file in order to be loaded.

# config/application.rbconfig.i18n.load_path +=Dir[Rails.root.join('config','locales','**','*.{rb,yml}')]

Place the shared localization options, such as date or currency formats, in files under the root of thelocales directory.

Use the short form of the I18n methods:I18n.t instead ofI18n.translate andI18n.l instead ofI18n.localize.

Use "lazy" lookup for locale entries from views and controllers. Let’s say we have the following structure:

en:  users:    show:      title: 'User details page'

The value forusers.show.title can be looked up in the templateapp/views/users/show.html.haml like this:

# bad=t'users.show.title'# good=t'.title'

Use dot-separated locale keys instead of specifying the:scope option with an array or a single symbol.Dot-separated notation is easier to read and trace the hierarchy.

# badI18n.t:record_invalid,scope:[:activerecord,:errors,:messages]# goodI18n.t:record_invalid,scope:'activerecord.errors.messages'I18n.t'activerecord.errors.messages.record_invalid'# badI18n.t:title,scope::invitation# goodI18n.t'title.invitation'

More detailed information about the Rails I18n can be found in theRails Guides

Use theasset pipeline to leverage organization within your application.

Reserveapp/assets for custom stylesheets, javascripts, or images.

Uselib/assets for your own libraries that don’t really fit into the scope of the application.

Third party code such asjQuery orbootstrap should be placed invendor/assets.

When possible, use gemified versions of assets (e.g.jquery-rails,jquery-ui-rails,bootstrap-sass,zurb-foundation).

Name the mailersSomethingMailer.Without the Mailer suffix it isn’t immediately apparent what’s a mailer and which views are related to the mailer.

Provide both HTML and plain-text view templates.

Enable errors raised on failed mail delivery in your development environment.The errors are disabled by default.

# config/environments/development.rbconfig.action_mailer.raise_delivery_errors=true

Use a local SMTP server likeMailcatcher in development environment.

# config/environments/development.rbconfig.action_mailer.smtp_settings={address:'localhost',port:1025,# more settings}

Provide default settings for the host name.

# config/environments/development.rbconfig.action_mailer.default_url_options={host:"#{local_ip}:3000"}# config/environments/production.rbconfig.action_mailer.default_url_options={host:'your_site.com'}# in your mailer classdefault_url_options[:host]='your_site.com'

Format the from and to addresses properly.Use the following format:

# in your mailer classdefaultfrom:'Your Name <info@your_site.com>'

If you’re using Rails 6.1 or higher, you can use theemail_address_with_name method:

# in your mailer classdefaultfrom:email_address_with_name('info@your_site.com','Your Name')

Make sure that the e-mail delivery method for your test environment is set totest:

# config/environments/test.rbconfig.action_mailer.delivery_method=:test

The delivery method for development and production should besmtp:

# config/environments/development.rb, config/environments/production.rbconfig.action_mailer.delivery_method=:smtp

When sending html emails all styles should be inline, as some mail clients have problems with external styles.This however makes them harder to maintain and leads to code duplication.There are two similar gems that transform the styles and put them in the corresponding html tags:premailer-rails androadie.

Sending emails while generating page response should be avoided.It causes delays in loading of the page and request can timeout if multiple email are sent.To overcome this emails can be sent in background process with the help ofsidekiq gem.

Prefer Ruby 2.3’s safe navigation operator&. overActiveSupport#try!.

# badobj.try!:fly# goodobj&.fly

Prefer Ruby’s Standard Library methods overActiveSupport aliases.

# bad'the day'.starts_with?'th''the day'.ends_with?'ay'# good'the day'.start_with?'th''the day'.end_with?'ay'

Prefer Ruby’s Standard Library over uncommon Active Support extensions.

# bad(1..50).to_a.forty_two1.in?[1,2]'day'.in?'the day'# good(1..50).to_a[41][1,2].include?1'the day'.include?'day'

Prefer Ruby’s comparison operators over Active Support’sArray#inquiry, andString#inquiry.

# bad - String#inquiryruby='two'.inquiryruby.two?# goodruby='two'ruby =='two'# bad - Array#inquirypets=%w(catdog).inquirypets.gopher?# goodpets=%w(catdog)pets.include?'cat'

Prefer Active Support’sexclude? over Ruby’s negatedinclude?.

# bad!array.include?(2)!hash.include?(:key)!string.include?('substring')# goodarray.exclude?(2)hash.exclude?(:key)string.exclude?('substring')

If you’re using Ruby 2.3 or higher, prefer squiggly heredoc (<<~) over Active Support’sstrip_heredoc.

# bad<<EOS.strip_heredoc  some textEOS# bad<<-EOS.strip_heredoc  some textEOS# good<<~EOS  some textEOS

If you’re using Rails 7.0 or higher, preferto_fs overto_formatted_s.to_formatted_s is just too cumbersome for a method used that frequently.

# badtime.to_formatted_s(:db)date.to_formatted_s(:db)datetime.to_formatted_s(:db)42.to_formatted_s(:human)# goodtime.to_fs(:db)date.to_fs(:db)datetime.to_fs(:db)42.to_fs(:human)

Configure your timezone accordingly inapplication.rb.

config.time_zone='Eastern European Time'# optional - note it can be only :utc or :local (default is :utc)config.active_record.default_timezone=:local

Don’t useTime.parse.

# badTime.parse('2015-03-02 19:05:37')# => Will assume time string given is in the system's time zone.# goodTime.zone.parse('2015-03-02 19:05:37')# => Mon, 02 Mar 2015 19:05:37 EET +02:00

Don’t useString#to_time

# bad - assumes time string given is in the system's time zone.'2015-03-02 19:05:37'.to_time# goodTime.zone.parse('2015-03-02 19:05:37')# => Mon, 02 Mar 2015 19:05:37 EET +02:00

Don’t useTime.now.

# badTime.now# => Returns system time and ignores your configured time zone.# goodTime.zone.now# => Fri, 12 Mar 2014 22:04:47 EET +02:00Time.current# Same thing but shorter.

Preferall_(day|week|month|quarter|year) overbeginning_of_(day|week|month|quarter|year)..end_of_(day|week|month|quarter|year)to get the range of date/time.

# baddate.beginning_of_day..date.end_of_daydate.beginning_of_week..date.end_of_weekdate.beginning_of_month..date.end_of_monthdate.beginning_of_quarter..date.end_of_quarterdate.beginning_of_year..date.end_of_year# gooddate.all_daydate.all_weekdate.all_monthdate.all_quarterdate.all_year

If used without a parameter, preferfrom_now andago instead ofsince,after,until orbefore.

# bad - It's not clear that the qualifier refers to the current time (which is the default parameter)5.hours.since5.hours.after5.hours.before5.hours.until# good5.hours.from_now5.hours.ago

If used with a parameter, prefersince,after,until orbefore instead offrom_now andago.

# bad - It's confusing and misleading to read2.days.from_now(yesterday)2.days.ago(yesterday)# good2.days.since(yesterday)2.days.after(yesterday)2.days.before(yesterday)2.days.until(yesterday)

Avoid using negative numbers for the duration subject. Always prefer using a qualifier that allows using positive literal numbers.

# bad - It's confusing and misleading to read-5.hours.from_now-5.hours.ago# good5.hours.ago5.hours.from_now

Use Duration methods instead of adding and subtracting with the current time.

# badTime.current -1.minuteTime.zone.now +2.days# good1.minute.ago2.days.from_now

Put gems used only for development or testing in the appropriate group in the Gemfile.

Use only established gems in your projects.If you’re contemplating on including some little-known gem you should do a careful review of its source code first.

Do not remove theGemfile.lock from version control.This is not some randomly generated file - it makes sure that all of your team members get the same gem versions when they do abundle install.

Prefer integration style controller tests over functional style controller tests,as recommended in the Rails documentation.

# badclassMyControllerTest <ActionController::TestCaseend# goodclassMyControllerTest <ActionDispatch::IntegrationTestend
# badtravel_to(Time.now)travel_to(DateTime.now)travel_to(Time.current)travel_to(Time.zone.now)travel_to(Time.now.in_time_zone)travel_to(Time.current.to_time)# goodfreeze_time

If your projects depends on various external processes useforeman to manage them.

Nothing written in this guide is set in stone.It’s my desire to work together with everyone interested in Rails coding style, so that we could ultimately create a resource that will be beneficial to the entire Ruby community.

Feel free to open tickets or send pull requests with improvements.Thanks in advance for your help!

You can also support the project (and RuboCop) with financial contributions viaPatreon.

It’s easy, just follow the contribution guidelines below:

A community-driven style guide is of little use to a community that doesn’t know about its existence.Tweet about the guide, share it with your friends and colleagues.Every comment, suggestion or opinion we get makes the guide just a little bit better.And we want to have the best possible guide, don’t we?

Cheers,
Bozhidar


[8]ページ先頭

©2009-2025 Movatter.jp