From 08756103b3a3433d108af396338e903cf61b45d1 Mon Sep 17 00:00:00 2001 From: Dennis Ideler Date: Fri, 4 Apr 2014 12:41:26 -0400 Subject: [PATCH 001/191] Remove mention of update_attribute being deprecated and removed The method was removed about 2 years ago, but that change has since been reverted: https://github.com/rails/rails/pull/6738#issuecomment-39584005 I also linked the method to its documentation on the RoR API docs: http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e6c2adf5..da80df3a 100644 --- a/README.md +++ b/README.md @@ -372,8 +372,8 @@ scopes like this. end ``` -* Beware of the behavior of the `update_attribute` method. It doesn't - run the model validations (unlike `update_attributes`) and could easily corrupt the model state. The method was finally deprecated in Rails 3.2.7 and does not exist in Rails 4. +* Beware of the behavior of the [`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute) method. It doesn't + run the model validations (unlike `update_attributes`) and could easily corrupt the model state. * Use user-friendly URLs. Show some descriptive attribute of the model in the URL rather than its `id`. There is more than one way to achieve this: * Override the `to_param` method of the model. This method is used by Rails for constructing a URL to the object. From ce39299e15dedbce9a599356e0207a4914848464 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 25 Apr 2014 16:15:41 +0300 Subject: [PATCH 002/191] Mention gittip --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index da80df3a..de735778 100644 --- a/README.md +++ b/README.md @@ -1482,6 +1482,11 @@ 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 via [gittip](https://www.gittip.com/bbatsov). + +[![Support via Gittip](https://rawgithub.com/twolfson/gittip-badge/0.2.0/dist/gittip.png)](https://www.gittip.com/bbatsov) + ## How to Contribute? It's easy, just follow the [contribution guidelines](https://github.com/bbatsov/rails-style-guide/blob/master/CONTRIBUTING.md). From 2851076c4aa59f5488c1dcf1c515128bff4e2706 Mon Sep 17 00:00:00 2001 From: MunkiPhD Date: Tue, 6 May 2014 13:48:41 -0400 Subject: [PATCH 003/191] fixed error where it was checking the incorrect object for validation errors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de735778..3976706e 100644 --- a/README.md +++ b/README.md @@ -1381,7 +1381,7 @@ which should be validated. Using `be_valid` does not guarantee that the problem describe '#title' do it 'is unique' do another_article = Fabricate.build(:article, title: article.title) - article.should have(1).error_on(:title) + another_article.should have(1).error_on(:title) end end end From 232a4fa58f007db219a25e57126e3b20ce796ff8 Mon Sep 17 00:00:00 2001 From: Lukas Elmer Date: Mon, 19 May 2014 23:57:11 +0200 Subject: [PATCH 004/191] Change cancan to cancancan --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3976706e..c2e9819d 100644 --- a/README.md +++ b/README.md @@ -721,10 +721,10 @@ compliant) that are useful in many Rails projects: application and notify you when you should add eager loading (N+1 queries), when you’re using eager loading that isn’t necessary and when you should use counter cache. -* [cancan](https://github.com/ryanb/cancan) - CanCan is an authorization gem that - lets you restrict users access to resources. All permissions are defined in a - single file (ability.rb) and convenient methods for checking and ensuring - permissions are available throughout the application. +* [cancancan](https://github.com/CanCanCommunity/cancancan) - CanCanCan is an + authorization gem that lets you restrict users access to resources. All permissions + are defined in a single file (ability.rb) and convenient methods for checking and + ensuring permissions are available throughout the application. * [capybara](https://github.com/jnicklas/capybara) - Capybara aims to simplify the process of integration testing Rack applications, such as Rails, Sinatra or Merb. Capybara simulates how a real user would interact with a web From 61151bfe55c65001d70959264a4d0fd4918690c1 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 21 May 2014 18:14:47 +0300 Subject: [PATCH 005/191] Remove gem suggestions and testing sections Those never really belonged here, so we're all better off without them. --- README.md | 805 +++--------------------------------------------------- 1 file changed, 33 insertions(+), 772 deletions(-) diff --git a/README.md b/README.md index c2e9819d..af601679 100644 --- a/README.md +++ b/README.md @@ -8,21 +8,7 @@ prescriptions for Ruby on Rails 3 & 4 development. It's a complementary guide to the already existing community-driven [Ruby coding style guide](https://github.com/bbatsov/ruby-style-guide). -While in the guide the section [Testing Rails applications](#testing-rails-applications) -is after [Developing Rails applications](#developing-rails-applications) I truly believe -that -[Behaviour-Driven Development](http://en.wikipedia.org/wiki/Behavior_Driven_Development) -(BDD) is the best way to develop software. Keep that in mind. - -Rails is an opinionated framework and this is an opinionated guide. In -my mind I'm totally certain that -[RSpec](https://www.relishapp.com/rspec) is superior to Test::Unit, -[Sass](http://sass-lang.com/) is superior to CSS and -[Haml](http://haml-lang.com/) ([Slim](http://slim-lang.com/)) is -superior to Erb. So don't expect to find any Test::Unit, CSS or Erb -advice in here. - -Some of the advice here is applicable only to Rails 3.1+. +Some of the advice here is applicable only to Rails 4.0+. You can generate a PDF or an HTML copy of this guide using [Transmuter](https://github.com/TechnoGate/transmuter). @@ -32,27 +18,38 @@ Translations of the guide are available in the following languages: * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) -# Table of Contents - -* [Developing Rails applications](#developing-rails-applications) - * [Configuration](#configuration) - * [Routing](#routing) - * [Controllers](#controllers) - * [Models](#models) - * [Migrations](#migrations) - * [Views](#views) - * [Internationalization](#internationalization) - * [Assets](#assets) - * [Mailers](#mailers) - * [Bundler](#bundler) - * [Priceless Gems](#priceless-gems) - * [Flawed Gems](#flawed-gems) - * [Managing processes](#managing-processes) -* [Testing Rails applications](#testing-rails-applications) - * [Cucumber](#cucumber) - * [RSpec](#rspec) - -# Developing Rails applications +# The Rails 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. + +## Table of Contents + +* [Configuration](#configuration) +* [Routing](#routing) +* [Controllers](#controllers) +* [Models](#models) +* [Migrations](#migrations) +* [Views](#views) +* [Internationalization](#internationalization) +* [Assets](#assets) +* [Mailers](#mailers) +* [Bundler](#bundler) +* [Priceless Gems](#priceless-gems) +* [Flawed Gems](#flawed-gems) +* [Managing processes](#managing-processes) ## Configuration @@ -701,109 +698,6 @@ specific gems to a `linux` group: some randomly generated file - it makes sure that all of your team members get the same gem versions when they do a `bundle install`. -## Priceless Gems - -One of the most important programming principles is "Don't reinvent -the wheel!". If you're faced with a certain task you should always -look around a bit for existing solutions, before rolling your -own. Here's a list of some "priceless" gems (all of them Rails 3.1 -compliant) that are useful in many Rails projects: - -* [active_admin](https://github.com/gregbell/active_admin) - With ActiveAdmin - the creation of admin interface for your Rails app is child's play. You get a - nice dashboard, CRUD UI and lots more. Very flexible and customizable. -* [better_errors](https://github.com/charliesome/better_errors) - Better Errors replaces - the standard Rails error page with a much better and more useful error page. It is also - usable outside of Rails in any Rack app as Rack middleware. -* [bullet](https://github.com/flyerhzm/bullet) - The Bullet gem is designed to - help you increase your application’s performance by reducing the number of - queries it makes. It will watch your queries while you develop your - application and notify you when you should add eager loading (N+1 queries), - when you’re using eager loading that isn’t necessary and when you should use - counter cache. -* [cancancan](https://github.com/CanCanCommunity/cancancan) - CanCanCan is an - authorization gem that lets you restrict users access to resources. All permissions - are defined in a single file (ability.rb) and convenient methods for checking and - ensuring permissions are available throughout the application. -* [capybara](https://github.com/jnicklas/capybara) - Capybara aims to simplify - the process of integration testing Rack applications, such as Rails, Sinatra - or Merb. Capybara simulates how a real user would interact with a web - application. It is agnostic about the driver running your tests and currently - comes with Rack::Test and Selenium support built in. HtmlUnit, WebKit and - env.js are supported through external gems. Works great in combination with - RSpec & Cucumber. -* [carrierwave](https://github.com/jnicklas/carrierwave) - the ultimate file - upload solution for Rails. Support both local and cloud storage for the - uploaded files (and many other cool things). Integrates great with - ImageMagick for image post-processing. -* [compass-rails](https://github.com/chriseppstein/compass) - Great gem that - adds support for some css frameworks. Includes collection of sass mixins that - reduces code of css files and help fight with browser incompatibilities. -* [cucumber-rails](https://github.com/cucumber/cucumber-rails) - Cucumber is - the premium tool to develop feature tests in Ruby. cucumber-rails provides - Rails integration for Cucumber. -* [devise](https://github.com/plataformatec/devise) - Devise is full-featured - authentication solution for Rails applications. In most cases it's preferable - to use devise to rolling your own custom authentication solution. -* [fabrication](http://fabricationgem.org/) - a great fixture replacement - (editor's choice). -* [factory_girl](https://github.com/thoughtbot/factory_girl) - an alternative - to fabrication. Nice and mature fixture replacement. Spiritual ancestor of - fabrication. -* [ffaker](https://github.com/EmmanuelOga/ffaker) - handy gem to generate dummy data - (names, addresses, etc). -* [feedzirra](https://github.com/pauldix/feedzirra) - Very fast and flexible - RSS/Atom feed parser. -* [friendly_id](https://github.com/norman/friendly_id) - Allows creation of - human-readable URLs by using some descriptive attribute of the model instead - of its id. -* [globalize](https://github.com/globalize/globalize) - Rails I18n de-facto standard - library for ActiveRecord model/data translation. Globalize for Rails and is targeted - at ActiveRecord version 4.x. It is compatible with and builds on the new I18n API in - Ruby on Rails and adds model translations to ActiveRecord. For ActiveRecord 3.x users, - check on the [3-0-stable branch](https://github.com/globalize/globalize/tree/3-0-stable). -* [guard](https://github.com/guard/guard) - fantastic gem that monitors file - changes and invokes tasks based on them. Loaded with lots of useful - extension. Far superior to autotest and watchr. -* [haml-rails](https://github.com/indirect/haml-rails) - haml-rails provides - Rails integration for Haml. -* [haml](http://haml-lang.com) - HAML is a concise templating language, - considered by many (including yours truly) to be far superior to Erb. -* [kaminari](https://github.com/amatsuda/kaminari) - Great paginating solution. -* [machinist](https://github.com/notahat/machinist) - Fixtures aren't fun. - Machinist is. -* [rspec-rails](https://github.com/rspec/rspec-rails) - RSpec is a replacement - for Test::MiniTest. I cannot recommend highly enough RSpec. rspec-rails - provides Rails integration for RSpec. -* [sidekiq](https://github.com/mperham/sidekiq) - Sidekiq is probably - the easiest and most scalable way to run background jobs in your - Rails app. -* [simple_form](https://github.com/plataformatec/simple_form) - once you've - used simple_form (or formtastic) you'll never want to hear about Rails's - default forms. It has a great DSL for building forms and no opinion on - markup. -* [simplecov-rcov](https://github.com/fguillen/simplecov-rcov) - RCov formatter - for SimpleCov. Useful if you're trying to use SimpleCov with the Hudson - contininous integration server. -* [simplecov](https://github.com/colszowka/simplecov) - code coverage tool. - Unlike RCov it's fully compatible with Ruby 1.9. Generates great reports. - Must have! -* [slim](http://slim-lang.com) - Slim is a concise templating language, - considered by many far superior to HAML (not to mention Erb). The only thing - stopping me from using Slim massively is the lack of good support in major - editors/IDEs. Its performance is phenomenal. -* [spork](https://github.com/sporkrb/spork) - A DRb server for testing - frameworks (RSpec / Cucumber currently) that forks before each run to ensure - a clean testing state. Simply put it preloads a lot of test environment and - as consequence the startup time of your tests in greatly decreased. Absolute - must have! -* [sunspot](https://github.com/sunspot/sunspot) - SOLR powered full-text search - engine. - -This list is not exhaustive and other gems might be added to it along -the road. All of the gems on the list are field tested, have active -development and community and are known to be of good code quality. - ## Flawed Gems This is a list of gems that are either problematic or superseded by @@ -828,639 +722,6 @@ other popular, but flawed gems. * If your projects depends on various external processes use [foreman](https://github.com/ddollar/foreman) to manage them. -# Testing Rails applications - -The best approach to implementing new features is probably the BDD -approach. You start out by writing some high level feature tests -(generally written using Cucumber), then you use these tests to drive -out the implementation of the feature. First you write view specs for -the feature and use those specs to create the relevant -views. Afterwards you create the specs for the controller(s) that will -be feeding data to the views and use those specs to implement the -controller. Finally you implement the models specs and the models -themselves. - -## Cucumber - -* Tag your pending scenarios with `@wip` (work in progress). These -scenarios will not be taken into account and will not be marked as -failing. When finishing the work on a pending scenario and -implementing the functionality it tests, the tag `@wip` should be -removed in order to include this scenario in the test suite. -* Setup your default profile to exclude the scenarios tagged with -`@javascript`. They are testing using the browser and disabling them -is recommended to increase the regular scenarios execution speed. -* Setup a separate profile for the scenarios marked with `@javascript` tag. - * The profiles can be configured in the `cucumber.yml` file. - - ```Ruby - # definition of a profile: - profile_name: --tags @tag_name - ``` - - * A profile is run with the command: - - ``` - cucumber -p profile_name - ``` - -* If using [fabrication](http://fabricationgem.org/) for fixtures - replacement, use the predefined - [fabrication steps](http://fabricationgem.org/#!cucumber-steps) -* Do not use the old `web_steps.rb` step definitions! -[The web steps were removed from the latest version of Cucumber.](http://aslakhellesoy.com/post/11055981222/the-training-wheels-came-off) Their -usage leads to the creation of verbose scenarios that do not properly -reflect the application domain. -* When checking for the presence of an element with visible text - (link, button, etc.) check for the text, not the element id. This - can detect problems with the i18n. -* Create separate features for different functionality regarding the same kind of objects: - - ```Ruby - # bad - Feature: Articles - # ... feature implementation ... - - # good - Feature: Article Editing - # ... feature implementation ... - - Feature: Article Publishing - # ... feature implementation ... - - Feature: Article Search - # ... feature implementation ... - - ``` - -* Each feature has three main components - * Title - * Narrative - a short explanation what the feature is about. - * Acceptance criteria - the set of scenarios each made up of individual steps. -* The most common format is known as the Connextra format. - - ```Ruby - In order to [benefit] ... - A [stakeholder]... - Wants to [feature] ... - ``` - -This format is the most common but is not required, the narrative can -be free text depending on the complexity of the feature. - -* Use Scenario Outlines freely to keep the scenarios DRY. - - ```Ruby - Scenario Outline: User cannot register with invalid e-mail - When I try to register with an email "" - Then I should see the error message "" - - Examples: - |email |error | - | |The e-mail is required| - |invalid email |is not a valid e-mail | - ``` - -* The steps for the scenarios are in `.rb` files under the -`step_definitions` directory. The naming convention for the steps file -is `[description]_steps.rb`. The steps can be separated into -different files based on different criterias. It is possible to have -one steps file for each feature (`home_page_steps.rb`). There also -can be one steps file for all features for a particular object -(`articles_steps.rb`). -* Use multiline step arguments to avoid repetition - - ```Ruby - Scenario: User profile - Given I am logged in as a user "John Doe" with an e-mail "user@test.com" - When I go to my profile - Then I should see the following information: - |First name|John | - |Last name |Doe | - |E-mail |user@test.com| - - # the step: - Then /^I should see the following information:$/ do |table| - table.raw.each do |field, value| - find_field(field).value.should =~ /#{value}/ - end - end - ``` - -## RSpec - -* Use just one expectation per example. - - ```Ruby - # bad - describe ArticlesController do - #... - - describe 'GET new' do - it 'assigns new article and renders the new article template' do - get :new - assigns[:article].should be_a_new Article - response.should render_template :new - end - end - - # ... - end - - # good - describe ArticlesController do - #... - - describe 'GET new' do - it 'assigns a new article' do - get :new - assigns[:article].should be_a_new Article - end - - it 'renders the new article template' do - get :new - response.should render_template :new - end - end - - end - ``` - -* Make heavy use of `describe` and `context` -* Name the `describe` blocks as follows: - * use "description" for non-methods - * use pound "#method" for instance methods - * use dot ".method" for class methods - - ```Ruby - class Article - def summary - #... - end - - def self.latest - #... - end - end - - # the spec... - describe Article do - describe '#summary' do - #... - end - - describe '.latest' do - #... - end - end - ``` - -* Use [fabricators](http://fabricationgem.org/) to create test - objects. -* Make heavy use of mocks and stubs - - ```Ruby - # mocking a model - article = mock_model(Article) - - # stubbing a method - Article.stub(:find).with(article.id).and_return(article) - ``` - -* When mocking a model, use the `as_null_object` method. It tells the - output to listen only for messages we expect and ignore any other - messages. - - ```Ruby - article = mock_model(Article).as_null_object - ``` - -* Use `let` blocks instead of `before(:each)` blocks to create data for - the spec examples. `let` blocks get lazily evaluated. - - ```Ruby - # use this: - let(:article) { Fabricate(:article) } - - # ... instead of this: - before(:each) { @article = Fabricate(:article) } - ``` - -* Use `subject` when possible - - ```Ruby - describe Article do - subject { Fabricate(:article) } - - it 'is not published on creation' do - subject.should_not be_published - end - end - ``` - -* Use `specify` if possible. It is a synonym of `it` but is more readable when there is no docstring. - - ```Ruby - # bad - describe Article do - before { @article = Fabricate(:article) } - - it 'is not published on creation' do - @article.should_not be_published - end - end - - # good - describe Article do - let(:article) { Fabricate(:article) } - specify { article.should_not be_published } - end - ``` - - -* Use `its` when possible - - ```Ruby - # bad - describe Article do - subject { Fabricate(:article) } - - it 'has the current date as creation date' do - subject.creation_date.should == Date.today - end - end - - # good - describe Article do - subject { Fabricate(:article) } - its(:creation_date) { should == Date.today } - end - ``` - - -* Use `shared_examples` if you want to create a spec group that can be shared by many other tests. - - ```Ruby - # bad - describe Array do - subject { Array.new [7, 2, 4] } - - context "initialized with 3 items" do - its(:size) { should eq(3) } - end - end - - describe Set do - subject { Set.new [7, 2, 4] } - - context "initialized with 3 items" do - its(:size) { should eq(3) } - end - end - - #good - shared_examples "a collection" do - subject { described_class.new([7, 2, 4]) } - - context "initialized with 3 items" do - its(:size) { should eq(3) } - end - end - - describe Array do - it_behaves_like "a collection" - end - - describe Set do - it_behaves_like "a collection" - end - -### Views - -* The directory structure of the view specs `spec/views` matches the - one in `app/views`. For example the specs for the views in - `app/views/users` are placed in `spec/views/users`. -* The naming convention for the view specs is adding `_spec.rb` to the - view name, for example the view `_form.html.haml` has a - corresponding spec `_form.html.haml_spec.rb`. -* `spec_helper.rb` needs to be required in each view spec file. -* The outer `describe` block uses the path to the view without the - `app/views` part. This is used by the `render` method when it is - called without arguments. - - ```Ruby - # spec/views/articles/new.html.haml_spec.rb - require 'spec_helper' - - describe 'articles/new.html.haml' do - # ... - end - ``` - -* Always mock the models in the view specs. The purpose of the view is - only to display information. -* The method `assign` supplies the instance variables which the view - uses and are supplied by the controller. - - ```Ruby - # spec/views/articles/edit.html.haml_spec.rb - describe 'articles/edit.html.haml' do - it 'renders the form for a new article creation' do - assign( - :article, - mock_model(Article).as_new_record.as_null_object - ) - render - rendered.should have_selector('form', - method: 'post', - action: articles_path - ) do |form| - form.should have_selector('input', type: 'submit') - end - end - ``` - -* Prefer the capybara negative selectors over should_not with the positive. - - ```Ruby - # bad - page.should_not have_selector('input', type: 'submit') - page.should_not have_xpath('tr') - - # good - page.should have_no_selector('input', type: 'submit') - page.should have_no_xpath('tr') - ``` - -* When a view uses helper methods, these methods need to be - stubbed. Stubbing the helper methods is done on the `template` - object: - - ```Ruby - # app/helpers/articles_helper.rb - class ArticlesHelper - def formatted_date(date) - # ... - end - end - - # app/views/articles/show.html.haml - = "Published at: #{formatted_date(@article.published_at)}" - - # spec/views/articles/show.html.haml_spec.rb - describe 'articles/show.html.haml' do - it 'displays the formatted date of article publishing' do - article = mock_model(Article, published_at: Date.new(2012, 01, 01)) - assign(:article, article) - - template.stub(:formatted_date).with(article.published_at).and_return('01.01.2012') - - render - rendered.should have_content('Published at: 01.01.2012') - end - end - ``` - -* The helpers specs are separated from the view specs in the `spec/helpers` directory. - -### Controllers - -* Mock the models and stub their methods. Testing the controller should not depend on the model creation. -* Test only the behaviour the controller should be responsible about: - * Execution of particular methods - * Data returned from the action - assigns, etc. - * Result from the action - template render, redirect, etc. - - ```Ruby - # Example of a commonly used controller spec - # spec/controllers/articles_controller_spec.rb - # We are interested only in the actions the controller should perform - # So we are mocking the model creation and stubbing its methods - # And we concentrate only on the things the controller should do - - describe ArticlesController do - # The model will be used in the specs for all methods of the controller - let(:article) { mock_model(Article) } - - describe 'POST create' do - before { Article.stub(:new).and_return(article) } - - it 'creates a new article with the given attributes' do - Article.should_receive(:new).with(title: 'The New Article Title').and_return(article) - post :create, message: { title: 'The New Article Title' } - end - - it 'saves the article' do - article.should_receive(:save) - post :create - end - - it 'redirects to the Articles index' do - article.stub(:save) - post :create - response.should redirect_to(action: 'index') - end - end - end - ``` - -* Use context when the controller action has different behaviour depending on the received params. - - ```Ruby - # A classic example for use of contexts in a controller spec is creation or update when the object saves successfully or not. - - describe ArticlesController do - let(:article) { mock_model(Article) } - - describe 'POST create' do - before { Article.stub(:new).and_return(article) } - - it 'creates a new article with the given attributes' do - Article.should_receive(:new).with(title: 'The New Article Title').and_return(article) - post :create, article: { title: 'The New Article Title' } - end - - it 'saves the article' do - article.should_receive(:save) - post :create - end - - context 'when the article saves successfully' do - before { article.stub(:save).and_return(true) } - - it 'sets a flash[:notice] message' do - post :create - flash[:notice].should eq('The article was saved successfully.') - end - - it 'redirects to the Articles index' do - post :create - response.should redirect_to(action: 'index') - end - end - - context 'when the article fails to save' do - before { article.stub(:save).and_return(false) } - - it 'assigns @article' do - post :create - assigns[:article].should be_eql(article) - end - - it 're-renders the "new" template' do - post :create - response.should render_template('new') - end - end - end - end - ``` - -### Models - -* Do not mock the models in their own specs. -* Use fabrication to make real objects. -* It is acceptable to mock other models or child objects. -* Create the model for all examples in the spec to avoid duplication. - - ```Ruby - describe Article do - let(:article) { Fabricate(:article) } - end - ``` - -* Add an example ensuring that the fabricated model is valid. - - ```Ruby - describe Article do - it 'is valid with valid attributes' do - article.should be_valid - end - end - ``` - -* When testing validations, use `have(x).errors_on` to specify the attibute -which should be validated. Using `be_valid` does not guarantee that the problem - is in the intended attribute. - - ```Ruby - # bad - describe '#title' do - it 'is required' do - article.title = nil - article.should_not be_valid - end - end - - # prefered - describe '#title' do - it 'is required' do - article.title = nil - article.should have(1).error_on(:title) - end - end - ``` - -* Add a separate `describe` for each attribute which has validations. - - ```Ruby - describe Article do - describe '#title' do - it 'is required' do - article.title = nil - article.should have(1).error_on(:title) - end - end - end - ``` - -* When testing uniqueness of a model attribute, name the other object `another_object`. - - ```Ruby - describe Article do - describe '#title' do - it 'is unique' do - another_article = Fabricate.build(:article, title: article.title) - another_article.should have(1).error_on(:title) - end - end - end - ``` - -### Mailers - -* The model in the mailer spec should be mocked. The mailer should not depend on the model creation. -* The mailer spec should verify that: - * the subject is correct - * the sender e-mail is correct - * the receiver(s) e-mail is stated correctly - * the e-mail contains the required information - - ```Ruby - describe SubscriberMailer do - let(:subscriber) { mock_model(Subscription, email: 'johndoe@test.com', name: 'John Doe') } - - describe 'successful registration email' do - subject { SubscriptionMailer.successful_registration_email(subscriber) } - - its(:subject) { should == 'Successful Registration!' } - its(:from) { should == ['info@your_site.com'] } - its(:to) { should == [subscriber.email] } - - it 'contains the subscriber name' do - subject.body.encoded.should match(subscriber.name) - end - end - end - ``` - -### Uploaders - -* What we can test about an uploader is whether the images are resized correctly. -Here is a sample spec of a [carrierwave](https://github.com/jnicklas/carrierwave) image uploader: - - ```Ruby - - # rspec/uploaders/person_avatar_uploader_spec.rb - require 'spec_helper' - require 'carrierwave/test/matchers' - - describe PersonAvatarUploader do - include CarrierWave::Test::Matchers - - # Enable images processing before executing the examples - before(:all) do - UserAvatarUploader.enable_processing = true - end - - # Create a new uploader. The model is mocked as the uploading and resizing images does not depend on the model creation. - before(:each) do - @uploader = PersonAvatarUploader.new(mock_model(Person).as_null_object) - @uploader.store!(File.open(path_to_file)) - end - - # Disable images processing after executing the examples - after(:all) do - UserAvatarUploader.enable_processing = false - end - - # Testing whether image is no larger than given dimensions - context 'the default version' do - it 'scales down an image to be no larger than 256 by 256 pixels' do - @uploader.should be_no_larger_than(256, 256) - end - end - - # Testing whether image has the exact dimensions - context 'the thumb version' do - it 'scales down an image to be exactly 64 by 64 pixels' do - @uploader.thumb.should have_dimensions(64, 64) - end - end - end - - ``` - # Further Reading There are a few excellent resources on Rails style, that you should From bc9873911197fb1353c4123eed9192c1ac695eef Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 22 May 2014 15:45:53 +0300 Subject: [PATCH 006/191] Remove a dead entry from the table of contents --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index af601679..c0c2f48b 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ various highly regarded Rails programming resources. * [Assets](#assets) * [Mailers](#mailers) * [Bundler](#bundler) -* [Priceless Gems](#priceless-gems) * [Flawed Gems](#flawed-gems) * [Managing processes](#managing-processes) From 55c78d809ac3046d34463c5180f14c953c42dda3 Mon Sep 17 00:00:00 2001 From: Nicolas McCurdy Date: Thu, 22 May 2014 10:40:09 -0400 Subject: [PATCH 007/191] Add a link to Better Specs under the Further Reading section --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c0c2f48b..50c7b9d9 100644 --- a/README.md +++ b/README.md @@ -731,6 +731,7 @@ consider if you have time to spare: * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book) * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book) * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) +* [Better Specs for RSpec](http://betterspecs.org) # Contributing From cc3ceb6032b77175d1b91ad4b683383353ff4acb Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sat, 21 Jun 2014 19:33:13 +0100 Subject: [PATCH 008/191] Update link for The Rails Way book Change from The Rails 3 Way to The Rails 4 Way --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c0c2f48b..122ac9a3 100644 --- a/README.md +++ b/README.md @@ -726,7 +726,7 @@ other popular, but flawed gems. There are a few excellent resources on Rails style, that you should consider if you have time to spare: -* [The Rails 3 Way](http://www.amazon.com/Rails-Way-Addison-Wesley-Professional-Ruby/dp/0321601661) +* [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275) * [Ruby on Rails Guides](http://guides.rubyonrails.org/) * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book) * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book) From 899dd5d8869c487bdea909068372c9061d87ff6e Mon Sep 17 00:00:00 2001 From: Andrei Beliankou Date: Mon, 30 Jun 2014 14:09:41 +0200 Subject: [PATCH 009/191] Added the Russian translation to the translations list. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 122ac9a3..d34ee28a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Translations of the guide are available in the following languages: * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) +* [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) # The Rails Style Guide From 661921b68090bc6fcc2bddc5e3fe0365f48eda57 Mon Sep 17 00:00:00 2001 From: tolgaavci Date: Sat, 5 Jul 2014 19:23:27 +0300 Subject: [PATCH 010/191] Added the Turkish translation to the translations list. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d34ee28a..081c2b82 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ Translations of the guide are available in the following languages: * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) +* [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) # The Rails Style Guide From e73b5e607f05fb2fc375767c5912b73b79fc10db Mon Sep 17 00:00:00 2001 From: Luqi Pan Date: Sat, 12 Jul 2014 20:01:02 -0400 Subject: [PATCH 011/191] Added few words to make the contrast more clear --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 081c2b82..c7576e19 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ abbreviations. :through` allows additional attributes and validations on the join model. ```Ruby - # using has_and_belongs_to_many + # not so good - using has_and_belongs_to_many class User < ActiveRecord::Base has_and_belongs_to_many :groups end From 378a42501c4279a365e84db9f0ae5c34e6333f9d Mon Sep 17 00:00:00 2001 From: Bruno Sutic Date: Wed, 16 Jul 2014 17:30:18 +0200 Subject: [PATCH 012/191] Update double to single quotes within code blocks It can be observed this style guide uses single quotes within code blocks. This update makes the guide more consistent in regards to this. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 081c2b82..a8a9b33c 100644 --- a/README.md +++ b/README.md @@ -414,7 +414,7 @@ There is more than one way to achieve this: person.do_awesome_stuff end - Person.where("age > 21").each do |person| + Person.where('age > 21').each do |person| person.party_all_night! end @@ -423,7 +423,7 @@ There is more than one way to achieve this: person.do_awesome_stuff end - Person.where("age > 21").find_each do |person| + Person.where('age > 21').find_each do |person| person.party_all_night! end ``` @@ -506,7 +506,7 @@ use the `activerecord` scope: user: Member attributes: user: - name: "Full name" + name: 'Full name' ``` Then `User.model_name.human` will return "Member" and @@ -538,7 +538,7 @@ following structure: en: users: show: - title: "User details page" + title: 'User details page' ``` The value for `users.show.title` can be looked up in the template From d528e95b394835c204a73b59925e0171ae81f96c Mon Sep 17 00:00:00 2001 From: Tod Beardsley Date: Sun, 20 Jul 2014 20:20:46 -0500 Subject: [PATCH 013/191] Add anchors to all bullets This is very similiar to the formatting used in https://github.com/bbatsov/ruby-style-guide/pull/338 and was requested by @bbatsov to be applied to this Rails guide as well. Much easier the second time around. :) --- README.md | 1391 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 787 insertions(+), 604 deletions(-) diff --git a/README.md b/README.md index a8a9b33c..9a351cd3 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ > -- Officer Alex J. Murphy / RoboCop The goal of this guide is to present a set of best practices and style -prescriptions for Ruby on Rails 3 & 4 development. It's a complementary -guide to the already existing community-driven +prescriptions for Ruby on Rails 3 & 4 development. It's a +complementary guide to the already existing community-driven [Ruby coding style guide](https://github.com/bbatsov/ruby-style-guide). Some of the advice here is applicable only to Rails 4.0+. @@ -25,17 +25,18 @@ Translations of the guide are available in the following languages: 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. +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). +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. +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. ## Table of Contents @@ -54,679 +55,862 @@ various highly regarded Rails programming resources. ## Configuration -* Put custom initialization code in `config/initializers`. The code in +* + Put custom initialization code in `config/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 example `carrierwave.rb`, - `active_admin.rb`, etc. -* Adjust accordingly the settings for development, test and production - environment (in the corresponding files under `config/environments/`) - * Mark additional assets for precompilation (if any): - - ```Ruby - # 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.css rails_admin/rails_admin.js ) - ``` - -* Keep configuration that's applicable to all environments in the `config/application.rb` file. -* Create an additional `staging` environment that closely resembles -the `production` one. - -## Routing - -* When you need to add more actions to a RESTful resource (do you - really need them at all?) use `member` and `collection` routes. - - ```Ruby - # bad - get 'subscriptions/:id/unsubscribe' - resources :subscriptions - - # good - resources :subscriptions do - get 'unsubscribe', on: :member - end - - # bad - get 'photos/search' - resources :photos - - # good - resources :photos do - get 'search', on: :collection - end - ``` - -* If you need to define multiple `member/collection` routes use the - alternative block syntax. +[[link](#config-initializers)] - ```Ruby - resources :subscriptions do - member do - get 'unsubscribe' - # more routes - end - end +* + Keep initialization code for each gem in a separate file with the same name + as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc. +[[link](#gem-initializers)] - resources :photos do - collection do - get 'search' - # more routes - end - end - ``` +* + Adjust accordingly the settings for development, test and production + environment (in the corresponding files under `config/environments/`) +[[link](#dev-test-prod-configs)] -* Use nested routes to express better the relationship between - ActiveRecord models. + * Mark additional assets for precompilation (if any): ```Ruby - class Post < ActiveRecord::Base - has_many :comments - end - - class Comments < ActiveRecord::Base - belongs_to :post - end - - # routes.rb - resources :posts do - resources :comments - end + # 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.css rails_admin/rails_admin.js ) ``` -* Use namespaced routes to group related actions. +* + Keep configuration that's applicable to all environments in the + `config/application.rb` file. +[[link](#app-config)] - ```Ruby - namespace :admin do - # Directs /admin/products/* to Admin::ProductsController - # (app/controllers/admin/products_controller.rb) - resources :products - end - ``` - -* Never use the legacy wild controller route. This route will make all - actions in every controller accessible via GET requests. +* + Create an additional `staging` environment that closely resembles the + `production` one. +[[link](#staging-like-prod)] - ```Ruby - # very bad - match ':controller(/:action(/:id(.:format)))' - ``` +## Routing -* Don't use `match` to define any routes. It's removed from Rails 4. +* + When you need to add more actions to a RESTful resource (do you really need + them at all?) use `member` and `collection` routes. +[[link](#member-collection-routes)] + + ```Ruby + # bad + get 'subscriptions/:id/unsubscribe' + resources :subscriptions + + # good + resources :subscriptions do + get 'unsubscribe', on: :member + end + + # bad + get 'photos/search' + resources :photos + + # good + resources :photos do + get 'search', on: :collection + end + ``` + +* + If you need to define multiple `member/collection` routes use the + alternative block syntax. +[[link](#many-member-collection-routes)] + + ```Ruby + resources :subscriptions do + member do + get 'unsubscribe' + # more routes + end + end + + resources :photos do + collection do + get 'search' + # more routes + end + end + ``` + +* + Use nested routes to express better the relationship between ActiveRecord + models. +[[link](#nested-routes)] + + ```Ruby + class Post < ActiveRecord::Base + has_many :comments + end + + class Comments < ActiveRecord::Base + belongs_to :post + end + + # routes.rb + resources :posts do + resources :comments + end + ``` + +* + Use namespaced routes to group related actions. +[[link](#namespaced-routes)] + + ```Ruby + namespace :admin do + # Directs /admin/products/* to Admin::ProductsController + # (app/controllers/admin/products_controller.rb) + resources :products + end + ``` + +* + Never use the legacy wild controller route. This route will make all actions + in every controller accessible via GET requests. +[[link](#no-wild-routes)] + + ```Ruby + # very bad + match ':controller(/:action(/:id(.:format)))' + ``` + +* + Don't use `match` to define any routes. It's removed from Rails 4. +[[link](#no-match-routes)] ## Controllers -* 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. -* Share no more than two instance variables between a controller and a view. +* + 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). +[[link](#skinny-controllers)] -## Models - -* Introduce non-ActiveRecord model classes freely. -* Name the models with meaningful (but short) names without -abbreviations. -* If you need model objects that support ActiveRecord behavior(like - validation) use the - [ActiveAttr](https://github.com/cgriego/active_attr) gem. - - ```Ruby - class Message - include ActiveAttr::Model - - attribute :name - attribute :email - attribute :content - attribute :priority - - attr_accessible :name, :email, :content - - validates :name, presence: true - validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } - validates :content, length: { maximum: 500 } - end - ``` - - For a more complete example refer to the - [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr). - -### ActiveRecord +* + Each controller action should (ideally) invoke only one method other than an + initial find or new. +[[link](#one-method)] -* Avoid altering ActiveRecord defaults (table names, primary key, etc) - unless you have a very good reason (like a database that's not under - your control). +* + Share no more than two instance variables between a controller and a view. +[[link](#shared-instance-variables)] - ```Ruby - # bad - don't do this if you can modify the schema - class Transaction < ActiveRecord::Base - self.table_name = 'order' - ... - end - ``` - -* Group macro-style methods (`has_many`, `validates`, etc) in the - beginning of the class definition. - - ```Ruby - class User < ActiveRecord::Base - # keep the default scope first (if any) - default_scope { where(active: true) } - - # constants come up next - GENDERS = %w(male female) - - # afterwards we put attr related macros - attr_accessor :formatted_date_of_birth +## Models - attr_accessible :login, :first_name, :last_name, :email, :password +* + Introduce non-ActiveRecord model classes freely. +[[link](#model-classes)] - # followed by association macros - belongs_to :country +* + Name the models with meaningful (but short) names without abbreviations. +[[link](#meaningful-model-names)] - has_many :authentications, dependent: :destroy +* + If you need model objects that support ActiveRecord behavior(like + validation) use the [ActiveAttr](https://github.com/cgriego/active_attr) + gem. +[[link](#activeattr-gem)] - # and validation macros - validates :email, presence: true - validates :username, presence: true - validates :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} + ```Ruby + class Message + include ActiveAttr::Model - # next we have callbacks - before_save :cook - before_save :update_username_lower + attribute :name + attribute :email + attribute :content + attribute :priority - # other macros (like devise's) should be placed after the callbacks + attr_accessible :name, :email, :content - ... - end - ``` + validates :name, presence: true + validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } + validates :content, length: { maximum: 500 } + end + ``` -* Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many -:through` allows additional attributes and validations on the join model. + For a more complete example refer to the + [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr). - ```Ruby - # using has_and_belongs_to_many - class User < ActiveRecord::Base - has_and_belongs_to_many :groups - end +### ActiveRecord - class Group < ActiveRecord::Base - has_and_belongs_to_many :users - end +* + Avoid altering ActiveRecord defaults (table names, primary key, etc) unless + you have a very good reason (like a database that's not under your control). +[[link](#keep-ar-defaults)] - # prefered way - using has_many :through - class User < ActiveRecord::Base - has_many :memberships - has_many :groups, through: :memberships - end + ```Ruby + # bad - don't do this if you can modify the schema + class Transaction < ActiveRecord::Base + self.table_name = 'order' + ... + end + ``` - class Membership < ActiveRecord::Base - belongs_to :user - belongs_to :group - end +* + Group macro-style methods (`has_many`, `validates`, etc) in the beginning of + the class definition. +[[link](#macro-style-methods)] - class Group < ActiveRecord::Base - has_many :memberships - has_many :users, through: :memberships - end - ``` + ```Ruby + class User < ActiveRecord::Base + # keep the default scope first (if any) + default_scope { where(active: true) } -* Prefer `self[:attribute]` over `read_attribute(:attribute)`. + # constants come up next + GENDERS = %w(male female) - ```Ruby - # bad - def amount - read_attribute(:amount) * 100 - end + # afterwards we put attr related macros + attr_accessor :formatted_date_of_birth - # good - def amount - self[:amount] * 100 - end - ``` + attr_accessible :login, :first_name, :last_name, :email, :password -* Always use the new - ["sexy" validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). + # followed by association macros + belongs_to :country - ```Ruby - # bad - validates_presence_of :email + has_many :authentications, dependent: :destroy - # good + # and validation macros validates :email, presence: true - ``` - -* When a custom validation is used more than once or the validation is -some regular expression mapping, create a custom validator file. - - ```Ruby - # bad - class Person - validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } - end - - # good - class EmailValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i + validates :username, presence: true + validates :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 callbacks + before_save :cook + before_save :update_username_lower + + # other macros (like devise's) should be placed after the callbacks + + ... + end + ``` + +* + Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many + :through` allows additional attributes and validations on the join model. +[[link](#has-many-through)] + + ```Ruby + # using has_and_belongs_to_many + class User < ActiveRecord::Base + has_and_belongs_to_many :groups + end + + class Group < ActiveRecord::Base + has_and_belongs_to_many :users + end + + # prefered way - using has_many :through + class User < ActiveRecord::Base + has_many :memberships + has_many :groups, through: :memberships + end + + class Membership < ActiveRecord::Base + belongs_to :user + belongs_to :group + end + + class Group < ActiveRecord::Base + has_many :memberships + has_many :users, through: :memberships + end + ``` + +* + Prefer `self[:attribute]` over `read_attribute(:attribute)`. +[[link](#self-attribute)] + + ```Ruby + # bad + def amount + read_attribute(:amount) * 100 + end + + # good + def amount + self[:amount] * 100 + end + ``` + +* + Always use the new ["sexy" + validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). +[[link](#sexy-validations)] + + ```Ruby + # bad + validates_presence_of :email + + # good + validates :email, presence: true + ``` + +* + When a custom validation is used more than once or the validation is some + regular expression mapping, create a custom validator file. +[[link](#custom-validator-file)] + + ```Ruby + # bad + class Person + validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } + end + + # good + class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i + end + end + + class Person + validates :email, email: true + end + ``` + +* + Keep custom validators under `app/validators`. +[[link](#app-validators)] + +* + Consider extracting custom validators to a shared gem if you're maintaining + several related apps or the validators are generic enough. +[[link](#custom-validators-gem)] + +* + Use named scopes freely. +[[link](#named-scopes)] + + ```Ruby + class User < ActiveRecord::Base + scope :active, -> { where(active: true) } + scope :inactive, -> { where(active: false) } + + scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } + end + ``` + +* + Wrap named scopes in `lambdas` to initialize them lazily (this is only a + prescription in Rails 3, but is mandatory in Rails 4). +[[link](#named-scope-lambdas)] + + ```Ruby + # bad + class User < ActiveRecord::Base + scope :active, where(active: true) + scope :inactive, where(active: false) + + scope :with_orders, joins(:orders).select('distinct(users.id)') + end + + # good + class User < ActiveRecord::Base + scope :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 an `ActiveRecord::Relation` + object. Arguably you can define even simpler scopes like this. +[[link](#named-scope-class)] + + ```Ruby + class User < ActiveRecord::Base + def self.with_orders + joins(:orders).select('distinct(users.id)') + end + end + ``` + +* + Beware of the behavior of the + [`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute) + method. It doesn't run the model validations (unlike `update_attributes`) and + could easily corrupt the model state. +[[link](#beware-update-attribute)] + +* + Use user-friendly URLs. Show some descriptive attribute of the model in the URL + rather than its `id`. There is more than one way to achieve this: +[[link](#user-friendly-urls)] + + * Override the `to_param` method of the model. This method is used by Rails + for constructing a URL to the object. The default implementation returns + the `id` of the record as a String. It could be overridden to include another + human-readable attribute. + + ```Ruby + class Person + def to_param + "#{id} #{name}".parameterize + end end - end + ``` - class Person - validates :email, email: true - end - ``` - -* Keep custom validators under `app/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. + In order to convert this to a URL-friendly value, `parameterize` should be + canlled on the string. The `id` of the object needs to be at the beginning so + that it can be found by the `find` method of ActiveRecord. - ```Ruby - class User < ActiveRecord::Base - scope :active, -> { where(active: true) } - scope :inactive, -> { where(active: false) } + * Use the `friendly_id` gem. It allows creation of human-readable URLs by + using some descriptive attribute of the model instead of its `id`. - scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } - end - ``` - -* Wrap named scopes in `lambdas` to initialize them lazily (this is only a prescription in Rails 3, but is mandatory in Rails 4). - - ```Ruby - # bad - class User < ActiveRecord::Base - scope :active, where(active: true) - scope :inactive, where(active: false) - - scope :with_orders, joins(:orders).select('distinct(users.id)') - end - - # good - class User < ActiveRecord::Base - scope :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 an -`ActiveRecord::Relation` object. Arguably you can define even simpler -scopes like this. - - ```Ruby - class User < ActiveRecord::Base - def self.with_orders - joins(:orders).select('distinct(users.id)') + ```Ruby + class Person + extend FriendlyId + friendly_id :name, use: :slugged end - end - ``` + ``` -* Beware of the behavior of the [`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute) method. It doesn't - run the model validations (unlike `update_attributes`) and could easily corrupt the model state. -* Use user-friendly URLs. Show some descriptive attribute of the model in the URL rather than its `id`. -There is more than one way to achieve this: - * Override the `to_param` method of the model. This method is used by Rails for constructing a URL to the object. - The default implementation returns the `id` of the record as a String. - It could be overridden to include another human-readable attribute. - - ```Ruby - class Person - def to_param - "#{id} #{name}".parameterize - end - end - ``` + Check the [gem documentation](https://github.com/norman/friendly_id) for more + information about its usage. - In order to convert this to a URL-friendly value, `parameterize` should be called on the string. The `id` of the - object needs to be at the beginning so that it can be found by the `find` method of ActiveRecord. +* + Use `find_each` to iterate over a collection of AR objects. Looping through a + collection of records from the database (using the `all` 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. +[[link](#find-each)] - * Use the `friendly_id` gem. It allows creation of human-readable URLs by using some descriptive attribute of the model instead of its `id`. - ```Ruby - class Person - extend FriendlyId - friendly_id :name, use: :slugged - end - ``` + ```Ruby + # bad + Person.all.each do |person| + person.do_awesome_stuff + end - Check the [gem documentation](https://github.com/norman/friendly_id) for more information about its usage. + Person.where('age > 21').each do |person| + person.party_all_night! + end -* Use `find_each` to iterate over a collection of AR objects. Looping - through a collection of records from the database (using the `all` - 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. + # good + Person.all.find_each do |person| + person.do_awesome_stuff + end - - ```Ruby - # bad - Person.all.each do |person| - person.do_awesome_stuff - end - - Person.where('age > 21').each do |person| - person.party_all_night! - end - - # good - Person.all.find_each do |person| - person.do_awesome_stuff - end - - Person.where('age > 21').find_each do |person| - person.party_all_night! - end - ``` + Person.where('age > 21').find_each do |person| + person.party_all_night! + end + ``` ## Migrations -* Keep the `schema.rb` (or `structure.sql`) under version control. -* Use `rake db:schema:load` instead of `rake db:migrate` to initialize -an empty database. -* Use `rake db:test:prepare` to update the schema of the test database. -* Enforce default values in the migrations themselves instead of in - the application layer. - - ```Ruby - # bad - application enforced default value - def amount - self[:amount] or 0 - end - ``` - - 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. - -* Enforce foreign-key constraints. While ActiveRecord does not support -them natively, there some great third-party gems like -[schema_plus](https://github.com/lomba/schema_plus) and [foreigner](https://github.com/matthuhiggins/foreigner). - -* When writing constructive migrations (adding tables or columns), use - the new Rails 3.1 way of doing the migrations - use the `change` - method instead of `up` and `down` methods. - - - ```Ruby - # the old way - class AddNameToPeople < ActiveRecord::Migration - def up - add_column :people, :name, :string - end - - def down - remove_column :people, :name - end - end - - # the new prefered way - class AddNameToPeople < ActiveRecord::Migration - def change - add_column :people, :name, :string - end - end - ``` - -* Don't use model classes in migrations. The model classes are -constantly evolving and at some point in the future migrations that -used to work might stop, because of changes in the models used. +* + Keep the `schema.rb` (or `structure.sql`) under version control. +[[link](#schema-version)] + +* + Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty + database. +[[link](#db-schema-load)] + +* + Use `rake db:test:prepare` to update the schema of the test database. +[[link](#db-test-prepare)] + +* + Enforce default values in the migrations themselves instead of in the + application layer. +[[link](#default-migration-values)] + + ```Ruby + # bad - application enforced default value + def amount + self[:amount] or 0 + end + ``` + + 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. + +* + Enforce foreign-key constraints. While ActiveRecord does not support them + natively, there some great third-party gems like + [schema_plus](https://github.com/lomba/schema_plus) and + [foreigner](https://github.com/matthuhiggins/foreigner). +[[link](#foreign-key-constraints)] + +* + When writing constructive migrations (adding tables or columns), use the new + Rails 3.1 way of doing the migrations - use the `change` method instead of + `up` and `down` methods. +[[link](#change-vs-up-down)] + + ```Ruby + # the old way + class AddNameToPeople < ActiveRecord::Migration + def up + add_column :people, :name, :string + end + + def down + remove_column :people, :name + end + end + + # the new prefered way + class AddNameToPeople < ActiveRecord::Migration + def change + add_column :people, :name, :string + end + end + ``` + +* + Don't use model classes in migrations. The model classes are constantly + evolving and at some point in the future migrations that used to work might + stop, because of changes in the models used. +[[link](#no-model-class-migrations)] ## Views -* Never call the model layer directly from a view. -* Never make complex formatting in the views, export the formatting to - a method in the view helper or the model. -* Mitigate code duplication by using partial templates and layouts. +* + Never call the model layer directly from a view. +[[link](#no-direct-model-view)] -## Internationalization - -* 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 -the `config/locales` directory. -* When the labels of an ActiveRecord model need to be translated, -use the `activerecord` scope: - - ``` - en: - activerecord: - models: - user: Member - attributes: - user: - name: 'Full name' - ``` +* + Never make complex formatting in the views, export the formatting to a method + in the view helper or the model. +[[link](#no-complex-view-formatting)] - Then `User.model_name.human` will return "Member" and - `User.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 ActiveRecord -attributes. Place the locale files for the models in a folder `models` and -the texts used in the views in folder `views`. - * When organization of the locale files is done with additional - directories, these directories must be described in the `application.rb` - file in order to be loaded. - - ```Ruby - # config/application.rb - config.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 the `locales` directory. -* Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` -and `I18n.l` instead of `I18n.localize`. -* Use "lazy" lookup for the texts used in views. Let's say we have the -following structure: +* + Mitigate code duplication by using partial templates and layouts. +[[link](#partials)] - ``` - en: - users: - show: - title: 'User details page' - ``` - - The value for `users.show.title` can be looked up in the template - `app/views/users/show.html.haml` like this: - - ```Ruby - = t '.title' - ``` - -* Use the dot-separated keys in the controllers and models instead of -specifying the `:scope` option. The dot-separated call is easier to read and -trace the hierarchy. - - ```Ruby - # use this call - I18n.t 'activerecord.errors.messages.record_invalid' - - # instead of this - I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages] - ``` +## Internationalization -* More detailed information about the Rails i18n can be found in the [Rails -Guides] -(http://guides.rubyonrails.org/i18n.html) +* + 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 the + `config/locales` directory. +[[link](#locale-texts)] + +* + When the labels of an ActiveRecord model need to be translated, use the + `activerecord` scope: +[[link](#translated-labels)] + + ``` + en: + activerecord: + models: + user: Member + attributes: + user: + name: 'Full name' + ``` + + Then `User.model_name.human` will return "Member" and + `User.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 ActiveRecord + attributes. Place the locale files for the models in a folder `models` and the + texts used in the views in folder `views`. +[[link](#organize-locale-files)] + + * When organization of the locale files is done with additional directories, + these directories must be described in the `application.rb` file in order + to be loaded. + + ```Ruby + # config/application.rb + config.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 the `locales` directory. +[[link](#shared-localization)] + +* + Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` + and `I18n.l` instead of `I18n.localize`. +[[link](#short-i18n)] + +* + Use "lazy" lookup for the texts used in views. Let's say we have the following + structure: +[[link](#lazy-lookup)] + + ``` + en: + users: + show: + title: 'User details page' + ``` + + The value for `users.show.title` can be looked up in the template + `app/views/users/show.html.haml` like this: + + ```Ruby + = t '.title' + ``` + +* + Use the dot-separated keys in the controllers and models instead of specifying + the `:scope` option. The dot-separated call is easier to read and trace the + hierarchy. +[[link](#dot-separated-keys)] + + ```Ruby + # use this call + I18n.t 'activerecord.errors.messages.record_invalid' + + # instead of this + I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages] + ``` + +* + More detailed information about the Rails i18n can be found in the [Rails + Guides](http://guides.rubyonrails.org/i18n.html) +[[link](#i18n-guides)] ## Assets Use the [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within your application. -* Reserve `app/assets` for custom stylesheets, javascripts, or images. -* Use `lib/assets` for your own libraries, that doesn’t really fit into the scope of the application. -* Third party code such as [jQuery](http://jquery.com/) or [bootstrap](http://twitter.github.com/bootstrap/) - should be placed in `vendor/assets`. -* When possible, use gemified versions of assets (e.g. [jquery-rails](https://github.com/rails/jquery-rails), [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails), [bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass), [zurb-foundation](https://github.com/zurb/foundation)). +* + Reserve `app/assets` for custom stylesheets, javascripts, or images. +[[link](#reserve-app-assets)] + +* + Use `lib/assets` for your own libraries, that doesn’t really fit into the + scope of the application. +[[link](#lib-assets)] + +* + Third party code such as [jQuery](http://jquery.com/) or + [bootstrap](http://twitter.github.com/bootstrap/) should be placed in + `vendor/assets`. +[[link](#vendor-assets)] + +* + When possible, use gemified versions of assets (e.g. + [jquery-rails](https://github.com/rails/jquery-rails), + [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails), + [bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass), + [zurb-foundation](https://github.com/zurb/foundation)). +[[link](#gem-assets)] ## Mailers -* Name the mailers `SomethingMailer`. 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. - - ```Ruby - # config/environments/development.rb - - config.action_mailer.raise_delivery_errors = true - ``` - -* Use a local SMTP server like [Mailcatcher](https://github.com/sj26/mailcatcher) in the development environment. - - ```Ruby - # config/environments/development.rb - - config.action_mailer.smtp_settings = { - address: 'localhost', - port: 1025, - # more settings - } - ``` - -* Provide default settings for the host name. - - ```Ruby - # config/environments/development.rb - config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } - - - # config/environments/production.rb - config.action_mailer.default_url_options = { host: 'your_site.com' } - - # in your mailer class - default_url_options[:host] = 'your_site.com' - ``` - -* If you need to use a link to your site in an email, always use the - `_url`, not `_path` methods. The `_url` methods include the host - name and the `_path` methods don't. - - ```Ruby - # wrong - You can always find more info about this course - = link_to 'here', url_for(course_path(@course)) - - # right - You can always find more info about this course - = link_to 'here', url_for(course_url(@course)) - ``` - -* Format the from and to addresses properly. Use the following format: - - ```Ruby - # in your mailer class - default from: 'Your Name ' - ``` - -* Make sure that the e-mail delivery method for your test environment is set to `test`: - - ```Ruby - # config/environments/test.rb - - config.action_mailer.delivery_method = :test - ``` - -* The delivery method for development and production should be `smtp`: - - ```Ruby - # config/environments/development.rb, config/environments/production.rb - - config.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: +* + Name the mailers `SomethingMailer`. Without the Mailer suffix it isn't + immediately apparent what's a mailer and which views are related to the + mailer. +[[link](#mailer-name)] + +* + Provide both HTML and plain-text view templates. +[[link](#html-plain-email)] + +* + Enable errors raised on failed mail delivery in your development environment. + The errors are disabled by default. +[[link](#enable-delivery-errors)] + + ```Ruby + # config/environments/development.rb + + config.action_mailer.raise_delivery_errors = true + ``` + +* + Use a local SMTP server like + [Mailcatcher](https://github.com/sj26/mailcatcher) in the development + environment. +[[link](#local-smtp)] + + ```Ruby + # config/environments/development.rb + + config.action_mailer.smtp_settings = { + address: 'localhost', + port: 1025, + # more settings + } + ``` + +* + Provide default settings for the host name. +[[link](#default-hostname)] + + ```Ruby + # config/environments/development.rb + config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } + + # config/environments/production.rb + config.action_mailer.default_url_options = { host: 'your_site.com' } + + # in your mailer class + default_url_options[:host] = 'your_site.com' + ``` + +* + If you need to use a link to your site in an email, always use the `_url`, not + `_path` methods. The `_url` methods include the host name and the `_path` + methods don't. +[[link](#url-not-path-in-email)] + + ```Ruby + # bad + You can always find more info about this course + = link_to 'here', url_for(course_path(@course)) + + # good + You can always find more info about this course + = link_to 'here', url_for(course_url(@course)) + ``` + +* + Format the from and to addresses properly. Use the following format: +[[link](#email-addresses)] + + ```Ruby + # in your mailer class + default from: 'Your Name ' + ``` + +* + Make sure that the e-mail delivery method for your test environment is set to + `test`: +[[link](#delivery-method-test)] + + ```Ruby + # config/environments/test.rb + + config.action_mailer.delivery_method = :test + ``` + +* + The delivery method for development and production should be `smtp`: +[[link](#delivery-method-smtp)] + + ```Ruby + # config/environments/development.rb, config/environments/production.rb + + config.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](https://github.com/fphilipe/premailer-rails) and [roadie](https://github.com/Mange/roadie). +[[link](#inline-email-styles)] -* Sending emails while generating page response should be avoided. It causes +* + 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 of [sidekiq](https://github.com/mperham/sidekiq) gem. +[[link](#background-email)] ## Bundler -* 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. -* OS-specific gems will by default result in a constantly changing `Gemfile.lock` -for projects with multiple developers using different operating systems. -Add all OS X specific gems to a `darwin` group in the Gemfile, and all Linux -specific gems to a `linux` group: - - ```Ruby - # Gemfile - group :darwin do - gem 'rb-fsevent' - gem 'growl' - end - - group :linux do - gem 'rb-inotify' - end - ``` - - To require the appropriate gems in the right environment, add the - following to `config/application.rb`: - - ```Ruby - platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym - Bundler.require(platform) - ``` - -* Do not remove the `Gemfile.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 a `bundle install`. +* + Put gems used only for development or testing in the appropriate group in the + Gemfile. +[[link](#dev-test-gems)] + +* + 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. +[[link](#only-good-gems)] + +* + OS-specific gems will by default result in a constantly changing + `Gemfile.lock` for projects with multiple developers using different operating + systems. Add all OS X specific gems to a `darwin` group in the Gemfile, and + all Linux specific gems to a `linux` group: +[[link](#os-specific-gemfile-locks)] + + ```Ruby + # Gemfile + group :darwin do + gem 'rb-fsevent' + gem 'growl' + end + + group :linux do + gem 'rb-inotify' + end + ``` + + To require the appropriate gems in the right environment, add the + following to `config/application.rb`: + + ```Ruby + platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym + Bundler.require(platform) + ``` + +* + Do not remove the `Gemfile.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 a `bundle install`. +[[link](#gemfile-lock)] ## Flawed Gems This is a list of gems that are either problematic or superseded by other gems. You should avoid using them in your projects. -* [rmagick](http://rmagick.rubyforge.org/) - this gem is notorious for its memory consumption. Use -[minimagick](https://github.com/probablycorey/mini_magick) instead. -* [autotest](http://www.zenspider.com/ZSS/Products/ZenTest/) - old solution for running tests automatically. Far -inferior to [guard](https://github.com/guard/guard) and [watchr](https://github.com/mynyml/watchr). -* [rcov](https://github.com/relevance/rcov) - code coverage tool, not - compatible with Ruby 1.9. Use - [SimpleCov](https://github.com/colszowka/simplecov) instead. -* [therubyracer](https://github.com/cowboyd/therubyracer) - the use of - this gem in production is strongly discouraged as it uses a very large amount of +* [rmagick](http://rmagick.rubyforge.org/) - this gem is notorious for its + memory consumption. Use + [minimagick](https://github.com/probablycorey/mini_magick) instead. + +* [autotest](http://www.zenspider.com/ZSS/Products/ZenTest/) - old solution for + running tests automatically. Far inferior to + [guard](https://github.com/guard/guard) and + [watchr](https://github.com/mynyml/watchr). + +* [rcov](https://github.com/relevance/rcov) - code coverage tool, not compatible + with Ruby 1.9. Use [SimpleCov](https://github.com/colszowka/simplecov) + instead. + +* [therubyracer](https://github.com/cowboyd/therubyracer) - the use of this gem + in production is strongly discouraged as it uses a very large amount of memory. I'd suggest using `node.js` instead. -This list is also a work in progress. Please, let me know if you know -other popular, but flawed gems. +This list is also a work in progress. Please, let me know if you know other +popular, but flawed gems. ## Managing processes -* If your projects depends on various external processes use +* + If your projects depends on various external processes use [foreman](https://github.com/ddollar/foreman) to manage them. +[[link](#foreman)] # Further Reading -There are a few excellent resources on Rails style, that you should -consider if you have time to spare: +There are a few excellent resources on Rails style, that you should consider if +you have time to spare: * [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275) * [Ruby on Rails Guides](http://guides.rubyonrails.org/) @@ -736,16 +920,15 @@ consider if you have time to spare: # Contributing -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. +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 via [gittip](https://www.gittip.com/bbatsov). +You can also support the project (and RuboCop) with financial contributions via +[gittip](https://www.gittip.com/bbatsov). [![Support via Gittip](https://rawgithub.com/twolfson/gittip-badge/0.2.0/dist/gittip.png)](https://www.gittip.com/bbatsov) @@ -756,15 +939,15 @@ It's easy, just follow the [contribution guidelines](https://github.com/bbatsov/ # License ![Creative Commons License](http://i.creativecommons.org/l/by/3.0/88x31.png) -This work is licensed under a [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/deed.en_US) +This work is licensed under a [Creative Commons Attribution 3.0 Unported +License](http://creativecommons.org/licenses/by/3.0/deed.en_US) # Spread the Word -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? +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](https://twitter.com/bbatsov) From e9df89c866ea5199cc197ddf8b03eaace0f6272c Mon Sep 17 00:00:00 2001 From: David Davis Date: Fri, 25 Jul 2014 15:58:34 -0400 Subject: [PATCH 014/191] Added rule about before_destroy for issue #7 --- README.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.md b/README.md index 9a351cd3..464f00fe 100644 --- a/README.md +++ b/README.md @@ -506,6 +506,32 @@ programming resources. end ``` +* + Since [Rails creates callbacks for dependent + associations](https://github.com/rails/rails/issues/3458), always call + `before_destroy` callbacks that perform validation with `prepend: true`. + + ```Ruby + # bad (roles will be deleted automatically even if super_admin? is true) + has_many :roles, dependent: :destroy + + before_destroy :ensure_deletable + + def ensure_deletable + fail "Cannot delete super admin." if super_admin? + end + + # good + has_many :roles, dependent: :destroy + + before_destroy :ensure_deletable, prepend: true + + def ensure_deletable + fail "Cannot delete super admin." if super_admin? + end + ``` + + ## Migrations * From 548d2085caf62a3fd5aa2edcbac88306ca0f589c Mon Sep 17 00:00:00 2001 From: darkside Date: Mon, 4 Aug 2014 15:16:12 +0300 Subject: [PATCH 015/191] Remove any Rails 3 mentions --- README.md | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 9a351cd3..fec07e44 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > -- Officer Alex J. Murphy / RoboCop The goal of this guide is to present a set of best practices and style -prescriptions for Ruby on Rails 3 & 4 development. It's a +prescriptions for Ruby on Rails 4 development. It's a complementary guide to the already existing community-driven [Ruby coding style guide](https://github.com/bbatsov/ruby-style-guide). @@ -398,8 +398,8 @@ programming resources. ``` * - Wrap named scopes in `lambdas` to initialize them lazily (this is only a - prescription in Rails 3, but is mandatory in Rails 4). + Wrap named scopes in `lambdas` to initialize them lazily (this is + mandatory in Rails 4). [[link](#named-scope-lambdas)] ```Ruby @@ -517,10 +517,6 @@ programming resources. database. [[link](#db-schema-load)] -* - Use `rake db:test:prepare` to update the schema of the test database. -[[link](#db-test-prepare)] - * Enforce default values in the migrations themselves instead of in the application layer. @@ -548,9 +544,8 @@ programming resources. [[link](#foreign-key-constraints)] * - When writing constructive migrations (adding tables or columns), use the new - Rails 3.1 way of doing the migrations - use the `change` method instead of - `up` and `down` methods. + When writing constructive migrations (adding tables or columns), + use the `change` method instead of `up` and `down` methods. [[link](#change-vs-up-down)] ```Ruby From f65254cb92915a3503b5ce873186fd58840ff3d7 Mon Sep 17 00:00:00 2001 From: darkside Date: Mon, 4 Aug 2014 15:32:31 +0300 Subject: [PATCH 016/191] Remove info about mandatory lambdas in named scopes --- README.md | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/README.md b/README.md index fec07e44..960e35ac 100644 --- a/README.md +++ b/README.md @@ -397,29 +397,6 @@ programming resources. end ``` -* - Wrap named scopes in `lambdas` to initialize them lazily (this is - mandatory in Rails 4). -[[link](#named-scope-lambdas)] - - ```Ruby - # bad - class User < ActiveRecord::Base - scope :active, where(active: true) - scope :inactive, where(active: false) - - scope :with_orders, joins(:orders).select('distinct(users.id)') - end - - # good - class User < ActiveRecord::Base - scope :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 From d8d6df76696beedbbb0e6bf0e514c68844f38296 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 4 Aug 2014 15:55:03 +0300 Subject: [PATCH 017/191] [Fix #90] Add a mention of write_attribute --- README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ea5baac..58e3e097 100644 --- a/README.md +++ b/README.md @@ -323,9 +323,9 @@ programming resources. end ``` -* +* Prefer `self[:attribute]` over `read_attribute(:attribute)`. -[[link](#self-attribute)] +[[link](#read-attribute)] ```Ruby # bad @@ -339,6 +339,22 @@ programming resources. end ``` +* + Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. +[[link](#write-attribute)] + + ```Ruby + # bad + def amount + write_attributee(:amount, 100) + end + + # good + def amount + self[:amount] = 100 + end + ``` + * Always use the new ["sexy" validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). From 66c488b8e4c179eb9c953f13d3ebf505cdf41d48 Mon Sep 17 00:00:00 2001 From: Akshay Mohite Date: Mon, 4 Aug 2014 11:57:33 +0530 Subject: [PATCH 018/191] match can be used if request type is specified --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a351cd3..4e237b74 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ programming resources. ``` * - Don't use `match` to define any routes. It's removed from Rails 4. + Don't use `match` 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. [[link](#no-match-routes)] ## Controllers From 44b84aad41d73c1ef0b8582d2d4e8a3fe229bdad Mon Sep 17 00:00:00 2001 From: Satour Date: Wed, 13 Aug 2014 12:23:03 +0900 Subject: [PATCH 019/191] Add the Japanese translation to the translations list. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b28705d7..d365663f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Translations of the guide are available in the following languages: * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) +* [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) # The Rails Style Guide From 6b7cc72a322471c187619458f416e58c411f12b8 Mon Sep 17 00:00:00 2001 From: Dmitry Mozzherin Date: Wed, 13 Aug 2014 18:10:25 -0400 Subject: [PATCH 020/191] typos fixed --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d365663f..0b7cc8aa 100644 --- a/README.md +++ b/README.md @@ -347,7 +347,7 @@ programming resources. ```Ruby # bad def amount - write_attributee(:amount, 100) + write_attribute(:amount, 100) end # good @@ -455,7 +455,7 @@ programming resources. ``` In order to convert this to a URL-friendly value, `parameterize` should be - canlled on the string. The `id` of the object needs to be at the beginning so + called on the string. The `id` of the object needs to be at the beginning so that it can be found by the `find` method of ActiveRecord. * Use the `friendly_id` gem. It allows creation of human-readable URLs by From 7684fe2bb8de9079aa2eef5fa7833cb564bc21d9 Mon Sep 17 00:00:00 2001 From: Pavel Shpak Date: Fri, 15 Aug 2014 02:37:48 +0300 Subject: [PATCH 021/191] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b28705d7..868cd02f 100644 --- a/README.md +++ b/README.md @@ -454,7 +454,7 @@ programming resources. ``` In order to convert this to a URL-friendly value, `parameterize` should be - canlled on the string. The `id` of the object needs to be at the beginning so + called on the string. The `id` of the object needs to be at the beginning so that it can be found by the `find` method of ActiveRecord. * Use the `friendly_id` gem. It allows creation of human-readable URLs by From 211f3cf1a06597ba74f12a8e58a5aa0076893947 Mon Sep 17 00:00:00 2001 From: Pavel Shpak Date: Fri, 15 Aug 2014 02:40:29 +0300 Subject: [PATCH 022/191] delete not needed 'all' before 'find_each' it is shorter and more readable --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 868cd02f..cac8fbd0 100644 --- a/README.md +++ b/README.md @@ -490,7 +490,7 @@ programming resources. end # good - Person.all.find_each do |person| + Person.find_each do |person| person.do_awesome_stuff end From 7814f752b46bcc5d9b5de753c75c28b55398e991 Mon Sep 17 00:00:00 2001 From: Pavel Shpak Date: Fri, 15 Aug 2014 02:42:05 +0300 Subject: [PATCH 023/191] delete url_for --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cac8fbd0..e8e636c4 100644 --- a/README.md +++ b/README.md @@ -790,11 +790,11 @@ your application. ```Ruby # bad You can always find more info about this course - = link_to 'here', url_for(course_path(@course)) + = link_to 'here', course_path(@course) # good You can always find more info about this course - = link_to 'here', url_for(course_url(@course)) + = link_to 'here', course_url(@course) ``` * From 35f5ce9e724d9d31ac6c16b13d496cf811d6b06c Mon Sep 17 00:00:00 2001 From: swapdisc Date: Mon, 22 Sep 2014 19:08:50 -0700 Subject: [PATCH 024/191] Corrects grammar for #lib-assets section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a6fafe9..468ce66d 100644 --- a/README.md +++ b/README.md @@ -710,7 +710,7 @@ your application. [[link](#reserve-app-assets)] * - Use `lib/assets` for your own libraries, that doesn’t really fit into the + Use `lib/assets` for your own libraries that don’t really fit into the scope of the application. [[link](#lib-assets)] From 4b4a3d5722646c49935369407f0300524ce7ce39 Mon Sep 17 00:00:00 2001 From: Alejandro Cabrera Date: Thu, 2 Oct 2014 14:18:11 -0500 Subject: [PATCH 025/191] Remove exclusionary gendered constants GENDERS To improve the guide, replace the constants example with something that doesn't exclude a class of people. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 468ce66d..ab897f46 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,7 @@ programming resources. default_scope { where(active: true) } # constants come up next - GENDERS = %w(male female) + COLORS = %w(red green blue) # afterwards we put attr related macros attr_accessor :formatted_date_of_birth From 81d1dfb1fba334b24d9741e7e1b80f0d62bc449e Mon Sep 17 00:00:00 2001 From: JRice Date: Fri, 31 Oct 2014 20:00:02 -0400 Subject: [PATCH 026/191] Updated minimagick URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 468ce66d..761d3475 100644 --- a/README.md +++ b/README.md @@ -897,7 +897,7 @@ other gems. You should avoid using them in your projects. * [rmagick](http://rmagick.rubyforge.org/) - this gem is notorious for its memory consumption. Use - [minimagick](https://github.com/probablycorey/mini_magick) instead. + [minimagick](https://github.com/minimagick/minimagick) instead. * [autotest](http://www.zenspider.com/ZSS/Products/ZenTest/) - old solution for running tests automatically. Far inferior to From 17166df41c664377e487f769c7876a6d8c498034 Mon Sep 17 00:00:00 2001 From: Andrei Beliankou Date: Tue, 16 Dec 2014 15:05:24 +0100 Subject: [PATCH 027/191] Reordered the list of translations. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d30bfa2b..842582a8 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ Translations of the guide are available in the following languages: * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) +* [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) -* [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) # The Rails Style Guide From a350ae5ae0830f852e85ce90982d068db5252afa Mon Sep 17 00:00:00 2001 From: Drew Ulmer Date: Fri, 9 Jan 2015 12:28:36 -0600 Subject: [PATCH 028/191] Update README.md As of Rails 4.2, Rails now supports foreign key constraints! --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 842582a8..65004804 100644 --- a/README.md +++ b/README.md @@ -557,11 +557,8 @@ programming resources. the Rails app is impossible. * - Enforce foreign-key constraints. While ActiveRecord does not support them - natively, there some great third-party gems like - [schema_plus](https://github.com/lomba/schema_plus) and - [foreigner](https://github.com/matthuhiggins/foreigner). -[[link](#foreign-key-constraints)] + Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord + supports foreign key constraints natively. * When writing constructive migrations (adding tables or columns), From 600a3c4f64807cdf1297cb0d54364a40cfff3c9b Mon Sep 17 00:00:00 2001 From: Andrei Beliankou Date: Tue, 10 Feb 2015 21:58:25 +0100 Subject: [PATCH 029/191] Added back a missing link. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 65004804..3cbb903e 100644 --- a/README.md +++ b/README.md @@ -559,11 +559,12 @@ programming resources. * Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord supports foreign key constraints natively. + [[link](#foreign-key-constraints)] * When writing constructive migrations (adding tables or columns), use the `change` method instead of `up` and `down` methods. -[[link](#change-vs-up-down)] + [[link](#change-vs-up-down)] ```Ruby # the old way From 21fdad8371bbe04614f9927b5ec52c214bfbedd6 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 19 Feb 2015 09:32:26 +0200 Subject: [PATCH 030/191] Add a rule about where.not --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 3cbb903e..591f76e6 100644 --- a/README.md +++ b/README.md @@ -525,6 +525,19 @@ programming resources. end ``` +### ActiveRecord Queries + +* + Favor the use of `where.not` over SQL. +[[link](#where-not)] + + ```Ruby + # bad + User.where("id != ?", id) + + # good + User.where.not(id: id) + ``` ## Migrations From c39d5e7d85ae2e28c18caf1a0b1085b913e68da0 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 19 Feb 2015 09:33:32 +0200 Subject: [PATCH 031/191] Add missing link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 591f76e6..d87f8ced 100644 --- a/README.md +++ b/README.md @@ -504,6 +504,7 @@ programming resources. Since [Rails creates callbacks for dependent associations](https://github.com/rails/rails/issues/3458), always call `before_destroy` callbacks that perform validation with `prepend: true`. +[[link](#before_destroy)] ```Ruby # bad (roles will be deleted automatically even if super_admin? is true) From 43a9693aa9b9c1785160d1a855f37bb10cbd8446 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 19 Feb 2015 09:35:51 +0200 Subject: [PATCH 032/191] Update TOC --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d87f8ced..a9f5b8dd 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ programming resources. * [Routing](#routing) * [Controllers](#controllers) * [Models](#models) + * [ActiveRecord](#activerecord) + * [ActiveRecord Queries](#activerecord-queries) * [Migrations](#migrations) * [Views](#views) * [Internationalization](#internationalization) From c2a9e80832c0327bf02ad1649be3504f83c45540 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 19 Feb 2015 09:50:10 +0200 Subject: [PATCH 033/191] Add a rule about find vs where --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index a9f5b8dd..b81e09e6 100644 --- a/README.md +++ b/README.md @@ -530,6 +530,19 @@ programming resources. ### ActiveRecord Queries +* + Favor the use of `find` over `where` +when you need to retrieve a single record by id. +[[link](#find)] + + ```Ruby + # bad + User.where(id: id).take + + # good + User.find(id) + ``` + * Favor the use of `where.not` over SQL. [[link](#where-not)] From bd7fe63a5475761fcead3b6aaf9334caadfbdb1f Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 19 Feb 2015 10:08:25 +0200 Subject: [PATCH 034/191] Add a rule about find_by vs where --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index b81e09e6..ff359336 100644 --- a/README.md +++ b/README.md @@ -543,6 +543,19 @@ when you need to retrieve a single record by id. User.find(id) ``` +* + Favor the use of `find_by` over `where` +when you need to retrieve a single record by some attributes. +[[link](#find_by)] + + ```Ruby + # bad + User.where(first_name: 'Bruce', last_name: 'Wayne').first + + # good + User.find_by(first_name: 'Bruce', last_name: 'Wayne')) + ``` + * Favor the use of `where.not` over SQL. [[link](#where-not)] From bf3514e544890f5d8628f8324a03548a8b91a78a Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 19 Feb 2015 11:14:59 +0200 Subject: [PATCH 035/191] Add a rule about find_each --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index ff359336..a987ecde 100644 --- a/README.md +++ b/README.md @@ -556,6 +556,23 @@ when you need to retrieve a single record by some attributes. User.find_by(first_name: 'Bruce', last_name: 'Wayne')) ``` +* + Use `find_each` when you need to process a lot of records. +[[link](#find_each)] + + ```Ruby + # bad - loads all the records at once + # This is very inefficient when the users table has thousands of rows. + User.all.each do |user| + NewsMailer.weekly(user).deliver_now + end + + # good - records are retrieved in batches + User.find_each do |user| + NewsMailer.weekly(user).deliver_now + end + ``` + * Favor the use of `where.not` over SQL. [[link](#where-not)] From 52d579a63db50a8c3d89541f07dddd25a985e436 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 19 Feb 2015 17:22:11 +0200 Subject: [PATCH 036/191] Add a few rules for dealing with time --- README.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/README.md b/README.md index a987ecde..a88a5bbc 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ programming resources. * [Internationalization](#internationalization) * [Assets](#assets) * [Mailers](#mailers) +* [Time](#time) * [Bundler](#bundler) * [Flawed Gems](#flawed-gems) * [Managing processes](#managing-processes) @@ -901,6 +902,43 @@ your application. of [sidekiq](https://github.com/mperham/sidekiq) gem. [[link](#background-email)] +## Time + +* + Config your timezone accordingly in `application.rb`. +[[link](#time-now)] + + ```Ruby + config.time_zone = 'Eastern European Time' + # optional - note it can be only :utc or :local (default is :utc) + config.active_record.default_timezone = :local + ``` + +* ppppp + Don't use `Time.parse`. +[[link](#time-parse)] + + ```Ruby + # bad + Time.parse("2015-03-02 19:05:37") # => Will assume time string given is in the system's time zone. + + # good + Time.zone.parse(""2015-03-02 19:05:37") # => Mon, 02 Mar 2015 19:05:37 EET +02:00 + ``` + +* ppppp + Don't use `Time.now`. +[[link](#time-now)] + + ```Ruby + # bad + Time.now # => Returns system time and ignores your configured time zone. + + # good + Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 + Time.current # Same thing but shorter. + ``` + ## Bundler * From 52808b1999db08e129986b1b0a126d7d5c816dac Mon Sep 17 00:00:00 2001 From: Neilor Caldeira Date: Thu, 19 Feb 2015 16:26:47 -0200 Subject: [PATCH 037/191] Corrects code syntax for #time-parse section Updates #time.parse with single-quotes --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a88a5bbc..12e149e5 100644 --- a/README.md +++ b/README.md @@ -920,10 +920,10 @@ your application. ```Ruby # bad - Time.parse("2015-03-02 19:05:37") # => Will assume time string given is in the system's time zone. + Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone. # good - Time.zone.parse(""2015-03-02 19:05:37") # => Mon, 02 Mar 2015 19:05:37 EET +02:00 + Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 ``` * ppppp From a35a9c8d31f51ea803a386230569112b2cb4fd39 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 20 Feb 2015 20:46:06 +0200 Subject: [PATCH 038/191] Add a rule to avoid string interpolation in queries --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 12e149e5..d7b0fc81 100644 --- a/README.md +++ b/README.md @@ -531,6 +531,20 @@ programming resources. ### ActiveRecord Queries +* + Avoid string interpolation in + queries, as it will make your code susceptible to SQL injection + attacks. +[[link](#avoid-interpolation)] + + ```Ruby + # bad - param will be interpolated unescaped + Client.where("orders_count = #{params[:orders]}") + + # good - param will be properly escaped + Client.where('orders_count = ?', params[:orders]) + ``` + * Favor the use of `find` over `where` when you need to retrieve a single record by id. From 3c4a2d7275187a12c854df4c069e3a4db7842fbe Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 20 Feb 2015 20:53:12 +0200 Subject: [PATCH 039/191] Prefer named placeholders in queries with multiple placeholders --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index d7b0fc81..e24ed318 100644 --- a/README.md +++ b/README.md @@ -545,6 +545,25 @@ programming resources. Client.where('orders_count = ?', params[:orders]) ``` +* + Consider using named placeholders instead of positional placeholders + when you have more than 1 placeholder in your query. +[[link](#named-placeholder)] + + ```Ruby + # okish + Client.where( + 'created_at >= ? AND created_at <= ?', + params[:start_date], params[:end_date] + ) + + # good + Client.where( + 'created_at >= :start_date AND created_at <= :end_date', + start_date: params[:start_date], end_date: params[:end_date] + ) + ``` + * Favor the use of `find` over `where` when you need to retrieve a single record by id. From 00e132d637d06009db725841fbc057804efaaf31 Mon Sep 17 00:00:00 2001 From: Genadi Samokovarov Date: Sat, 21 Feb 2015 14:43:14 +0200 Subject: [PATCH 040/191] Fix a typppppo I can imagine that those extra `ppppp` characters were not typed deliberately. :beers: --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e24ed318..2a6c922f 100644 --- a/README.md +++ b/README.md @@ -947,7 +947,7 @@ your application. config.active_record.default_timezone = :local ``` -* ppppp +* Don't use `Time.parse`. [[link](#time-parse)] @@ -959,7 +959,7 @@ your application. Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 ``` -* ppppp +* Don't use `Time.now`. [[link](#time-now)] From 97161075b35f42cc58aa5ad6acd36f348d420688 Mon Sep 17 00:00:00 2001 From: ajfaraday Date: Sun, 1 Mar 2015 18:12:32 +0000 Subject: [PATCH 041/191] various changes as discussed in pull request #134, tidied in to one commit Changes are mostly syntax and consistency fixes, and a code example of the pitfalls of using class methods as scopes --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 2a6c922f..13f66202 100644 --- a/README.md +++ b/README.md @@ -215,9 +215,9 @@ programming resources. [[link](#meaningful-model-names)] * - If you need model objects that support ActiveRecord behavior(like - validation) use the [ActiveAttr](https://github.com/cgriego/active_attr) - gem. + If you need model objects that support ActiveRecord behavior (like validation) + without the ActiveRecord database functionality use the + [ActiveAttr](https://github.com/cgriego/active_attr) gem. [[link](#activeattr-gem)] ```Ruby @@ -367,9 +367,10 @@ programming resources. ```Ruby # bad validates_presence_of :email + validates_length_of :email, maximum: 100 # good - validates :email, presence: true + validates :email, presence: true, length: { maximum: 100 } ``` * @@ -422,6 +423,7 @@ programming resources. complicated, it is preferable to make a class method instead which serves the same purpose of the named scope and returns an `ActiveRecord::Relation` object. Arguably you can define even simpler scopes like this. + [[link](#named-scope-class)] ```Ruby @@ -432,6 +434,32 @@ programming resources. end ``` + Note that this style of scoping can not be chained in the same way as named scopes. For instance: + + ```Ruby + # unchainable + class User < ActiveRecord::Base + def User.old + where('age > ?', 80) + end + + def User.heavy + where('weight > ?', 200) + end + end + ``` + + In this style both `old` and `heavy` work individually, but you can not call `User.old.heavy`, to chain these scopes use: + + ```Ruby + # chainable + class User < ActiveRecord::Base + scope :old, -> { where('age > 60') } + scope :heavy, -> { where('weight > 200') } + end + ``` + + * Beware of the behavior of the [`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute) @@ -587,7 +615,7 @@ when you need to retrieve a single record by some attributes. User.where(first_name: 'Bruce', last_name: 'Wayne').first # good - User.find_by(first_name: 'Bruce', last_name: 'Wayne')) + User.find_by(first_name: 'Bruce', last_name: 'Wayne') ``` * @@ -779,15 +807,15 @@ when you need to retrieve a single record by some attributes. [[link](#dot-separated-keys)] ```Ruby - # use this call - I18n.t 'activerecord.errors.messages.record_invalid' - - # instead of this + # bad I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages] + + # good + I18n.t 'activerecord.errors.messages.record_invalid' ``` * - More detailed information about the Rails i18n can be found in the [Rails + More detailed information about the Rails I18n can be found in the [Rails Guides](http://guides.rubyonrails.org/i18n.html) [[link](#i18n-guides)] @@ -882,11 +910,11 @@ your application. ```Ruby # bad You can always find more info about this course - = link_to 'here', course_path(@course) + <%= link_to 'here', course_path(@course) %> # good You can always find more info about this course - = link_to 'here', course_url(@course) + <%= link_to 'here', course_url(@course) %> ``` * From c0bd5d91cd67ac9dd847f4bf62ebfedc70ac760f Mon Sep 17 00:00:00 2001 From: Vestimir Markov Date: Sun, 8 Mar 2015 19:56:50 +0200 Subject: [PATCH 042/191] Add more info about YAML config files. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 13f66202..daac50bd 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,16 @@ programming resources. `production` one. [[link](#staging-like-prod)] +* + Keep any additional configuration in YAML files under the `config/` directory. +[[link](#yaml-config)] + + Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method: + + ```Ruby + Rails::Application.config_for(:yaml_file) + ``` + ## Routing * From 2aa3a6bab0e53e52fe8a8fbd6191ebd5bcd51ee0 Mon Sep 17 00:00:00 2001 From: Justyna Rachowicz Date: Fri, 13 Mar 2015 22:26:44 +0100 Subject: [PATCH 043/191] Fix grammar --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index daac50bd..5d90e1dd 100644 --- a/README.md +++ b/README.md @@ -444,7 +444,7 @@ programming resources. end ``` - Note that this style of scoping can not be chained in the same way as named scopes. For instance: + Note that this style of scoping cannot be chained in the same way as named scopes. For instance: ```Ruby # unchainable @@ -459,7 +459,7 @@ programming resources. end ``` - In this style both `old` and `heavy` work individually, but you can not call `User.old.heavy`, to chain these scopes use: + In this style both `old` and `heavy` work individually, but you cannot call `User.old.heavy`, to chain these scopes use: ```Ruby # chainable From ba0ad6e443f5f3ae82bcfb2d5a7d855fb973e914 Mon Sep 17 00:00:00 2001 From: Daekwon Kim Date: Mon, 6 Apr 2015 21:31:19 +0900 Subject: [PATCH 044/191] Add korean translation link --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5d90e1dd..9237f198 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Translations of the guide are available in the following languages: * [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) +* [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md) # The Rails Style Guide From 8c4e750327a7938a960aa8270f572af9c649e7dd Mon Sep 17 00:00:00 2001 From: Andrei Beliankou Date: Wed, 13 May 2015 13:11:03 +0200 Subject: [PATCH 045/191] Added a link to the upcoming German translation. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9237f198..7f0c87bf 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Translations of the guide are available in the following languages: * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) +* [German](https://github.com/arbox/de-rails-style-guide/blob/master/README-deDE.md) * [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) From b139a490c2e625165bfb298f0458ddc2e19c3667 Mon Sep 17 00:00:00 2001 From: Kurt Kotzur Date: Sun, 28 Jun 2015 14:18:37 -0700 Subject: [PATCH 046/191] Add bullet for using heredocs with String#squish on multiline SQL --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 7f0c87bf..88e228f7 100644 --- a/README.md +++ b/README.md @@ -658,6 +658,33 @@ when you need to retrieve a single record by some attributes. # good User.where.not(id: id) ``` +* + When specifying an explicit query in a method such as `find_by_sql`, use + heredocs with `squish`. 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). +[[link](#squished-heredocs)] + + ```Ruby + User.find_by_sql(< Date: Fri, 10 Jul 2015 21:13:36 -0700 Subject: [PATCH 047/191] Fix tz-config link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 88e228f7..7619f154 100644 --- a/README.md +++ b/README.md @@ -1006,7 +1006,7 @@ your application. * Config your timezone accordingly in `application.rb`. -[[link](#time-now)] +[[link](#tz-config)] ```Ruby config.time_zone = 'Eastern European Time' From 537fb7e4473f4a2f109014430ff045b2a03cc5fb Mon Sep 17 00:00:00 2001 From: Alexandr Elhovenko Date: Sun, 12 Jul 2015 16:54:51 +0300 Subject: [PATCH 048/191] Add a note about shallow routes --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 7619f154..c7acc367 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,17 @@ programming resources. resources :comments end ``` + +* + If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`. + + ```Ruby + resources :posts, shallow: true do + resources :comments do + resources :versions + end + end + ``` * Use namespaced routes to group related actions. From aca8f14598b8fbfbe60252c57631bef1dabe788f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Sun, 9 Aug 2015 05:47:48 -0300 Subject: [PATCH 049/191] [Fix #151] Remove flawed gems section --- README.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.md b/README.md index c7acc367..a5789d68 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,6 @@ programming resources. * [Mailers](#mailers) * [Time](#time) * [Bundler](#bundler) -* [Flawed Gems](#flawed-gems) * [Managing processes](#managing-processes) ## Configuration @@ -1096,31 +1095,6 @@ your application. same gem versions when they do a `bundle install`. [[link](#gemfile-lock)] -## Flawed Gems - -This is a list of gems that are either problematic or superseded by -other gems. You should avoid using them in your projects. - -* [rmagick](http://rmagick.rubyforge.org/) - this gem is notorious for its - memory consumption. Use - [minimagick](https://github.com/minimagick/minimagick) instead. - -* [autotest](http://www.zenspider.com/ZSS/Products/ZenTest/) - old solution for - running tests automatically. Far inferior to - [guard](https://github.com/guard/guard) and - [watchr](https://github.com/mynyml/watchr). - -* [rcov](https://github.com/relevance/rcov) - code coverage tool, not compatible - with Ruby 1.9. Use [SimpleCov](https://github.com/colszowka/simplecov) - instead. - -* [therubyracer](https://github.com/cowboyd/therubyracer) - the use of this gem - in production is strongly discouraged as it uses a very large amount of - memory. I'd suggest using `node.js` instead. - -This list is also a work in progress. Please, let me know if you know other -popular, but flawed gems. - ## Managing processes * From 1a4b5a40440b5888e8a39fba784cac97db71d38e Mon Sep 17 00:00:00 2001 From: Lee Han Kyeol Date: Thu, 13 Aug 2015 16:20:08 +0900 Subject: [PATCH 050/191] Clarified internationalization submodules paths --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a5789d68..01d97ca0 100644 --- a/README.md +++ b/README.md @@ -807,8 +807,8 @@ when you need to retrieve a single record by some attributes. * Separate the texts used in the views from translations of ActiveRecord - attributes. Place the locale files for the models in a folder `models` and the - texts used in the views in folder `views`. + attributes. Place the locale files for the models in a folder `locales/models` and the + texts used in the views in folder `locales/views`. [[link](#organize-locale-files)] * When organization of the locale files is done with additional directories, From 1a99e1c6373b2119971259cabb918e54a477fa52 Mon Sep 17 00:00:00 2001 From: Andrew France Date: Tue, 25 Aug 2015 16:10:41 +0200 Subject: [PATCH 051/191] Remove incorrect content about chaining scopes --- README.md | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.md b/README.md index 01d97ca0..9ba7ae4f 100644 --- a/README.md +++ b/README.md @@ -456,32 +456,6 @@ programming resources. end ``` - Note that this style of scoping cannot be chained in the same way as named scopes. For instance: - - ```Ruby - # unchainable - class User < ActiveRecord::Base - def User.old - where('age > ?', 80) - end - - def User.heavy - where('weight > ?', 200) - end - end - ``` - - In this style both `old` and `heavy` work individually, but you cannot call `User.old.heavy`, to chain these scopes use: - - ```Ruby - # chainable - class User < ActiveRecord::Base - scope :old, -> { where('age > 60') } - scope :heavy, -> { where('weight > 200') } - end - ``` - - * Beware of the behavior of the [`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute) From acda0d4f6d6d72b5ec68abfb851f90b58c54c2c9 Mon Sep 17 00:00:00 2001 From: Dimitrios Zorbas Date: Thu, 3 Sep 2015 18:57:53 +0300 Subject: [PATCH 052/191] Add space before closing curly bracket --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ba7ae4f..3c66094f 100644 --- a/README.md +++ b/README.md @@ -305,7 +305,7 @@ programming resources. validates :username, presence: true validates :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} + validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true } # next we have callbacks before_save :cook From 3f0e1681bcdd09cf121c0652a6979a628153d6d6 Mon Sep 17 00:00:00 2001 From: Konstantinos Rousis Date: Thu, 15 Oct 2015 09:51:39 +0200 Subject: [PATCH 053/191] Use the new hash syntax --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9ba7ae4f..b64d7ffe 100644 --- a/README.md +++ b/README.md @@ -831,7 +831,7 @@ when you need to retrieve a single record by some attributes. ```Ruby # bad - I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages] + I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] # good I18n.t 'activerecord.errors.messages.record_invalid' From c96958e3df4671286242bc226fc217d388e0afa1 Mon Sep 17 00:00:00 2001 From: Augusts Bautra Date: Fri, 16 Oct 2015 12:14:42 +0300 Subject: [PATCH 054/191] Added enum macro recommendation Enums are a meaningful addition to Rails model ecosystem and can be used in the majority of models where a field can take a limited selection of values, but bool is insufficient. Placing their definition under attr macros is one way to go about it, another might be to put them above attr macros, right under constants. I'd be gald to hear others' opinion on the matter. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ed9cce0..9d16b112 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,10 @@ programming resources. attr_accessor :formatted_date_of_birth attr_accessible :login, :first_name, :last_name, :email, :password - + + # Rails4+ enums after attr macros, prefer the hash syntax + enum gender: { female: 0, male: 1 } + # followed by association macros belongs_to :country From e5f9148249a3582c02fe7e0465cb54b11c99afd7 Mon Sep 17 00:00:00 2001 From: Mauro George Date: Wed, 11 Nov 2015 19:59:58 -0200 Subject: [PATCH 055/191] Remove extra space on named-scope-class Also remove some extra spaces. --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0ed9cce0..55f801f2 100644 --- a/README.md +++ b/README.md @@ -171,10 +171,10 @@ programming resources. resources :comments end ``` - + * If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`. - + ```Ruby resources :posts, shallow: true do resources :comments do @@ -445,7 +445,6 @@ programming resources. complicated, it is preferable to make a class method instead which serves the same purpose of the named scope and returns an `ActiveRecord::Relation` object. Arguably you can define even simpler scopes like this. - [[link](#named-scope-class)] ```Ruby From b4d0f6551ae13f835596db4b1445706b927eb22d Mon Sep 17 00:00:00 2001 From: Alexander Lazarov Date: Sun, 22 Nov 2015 21:32:27 +0200 Subject: [PATCH 056/191] Add note about helper methods in models --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 0ed9cce0..7a4cc247 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,14 @@ programming resources. For a more complete example refer to the [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr). +* + 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-persistance + only. +[[link](#model-business-logic)] + ### ActiveRecord * From 782d29ce4c8d478772b9e7b1c377b1a93dd631fd Mon Sep 17 00:00:00 2001 From: Mauro George Date: Wed, 11 Nov 2015 20:15:17 -0200 Subject: [PATCH 057/191] Add the dependent option to the Active Record associations When we have a has many association without the `dependent` option like Chat has many messages and we destroy the Chat, the Message objects still have the `chat_id` reference pointing to a Chat that was destroyed. ``` [1] pry(main)> chat = Chat.first :id => 1, :name => "Chat 01" } [2] pry(main)> message = chat.messages.first :id => 1, :message => "message 1", :chat_id => 1, } [3] pry(main)> chat.destroy [4] pry(main)> message.reload :id => 1, :message => "message 1", :chat_id => 1, } [5] pry(main)> message.chat_id 1 [6] pry(main)> message.chat nil ``` This patch add the `dependent` option to the has many and has one associations to keep the things more explicit. --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 0ed9cce0..922f0fab 100644 --- a/README.md +++ b/README.md @@ -553,6 +553,22 @@ programming resources. end ``` +* + Define the `dependent` option to the `has_many` and `has_one` associations. +[[link](#has_many-has_one-dependent-option)] + + ```Ruby + # bad + class Post < ActiveRecord::Base + has_many :comments + end + + # good + class Post < ActiveRecord::Base + has_many :comments, dependent: :destroy + end + ``` + ### ActiveRecord Queries * From c690262e95255fef908fc65a96ab02da1adb9378 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Fri, 25 Dec 2015 13:44:56 +0200 Subject: [PATCH 058/191] Add ActiveSupport#try! tip --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 0ed9cce0..baccc556 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ programming resources. * [Internationalization](#internationalization) * [Assets](#assets) * [Mailers](#mailers) +* [Active Support Core Extensions](#active-support-core-extensions) * [Time](#time) * [Bundler](#bundler) * [Managing processes](#managing-processes) @@ -986,6 +987,22 @@ your application. of [sidekiq](https://github.com/mperham/sidekiq) gem. [[link](#background-email)] + +## Active Support Core Extensions + +* + Prefer Ruby 2.3.0's safe navigation operator `&.` over `ActiveSupport#try!`. +[[link](#try-bang)] + +```ruby +# bad +obj.try! :fly + +# good +obj&.fly +``` + + ## Time * From 49385e7bd0498e0f6854ef52bd5ef022a5c9e664 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Fri, 25 Dec 2015 16:16:50 +0200 Subject: [PATCH 059/191] Add ActiveSupport aliases usage tip --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index baccc556..c530d854 100644 --- a/README.md +++ b/README.md @@ -1002,6 +1002,21 @@ obj.try! :fly obj&.fly ``` +* + Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. +[[link](#active_support_aliases)] + +```ruby +# bad +'the day'.starts_with? 'th' +'the day'.ends_with? 'ay' + +# good +'the day'.start_with? 'th' +'the day'.end_with? 'ay' +``` + + ## Time From 656dc89694624b6432cdbbc6e59e322e4360a74f Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Sun, 27 Dec 2015 20:46:19 +0200 Subject: [PATCH 060/191] Add Controllers Rendering section and inline rendering tip --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 0ed9cce0..89a69786 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ programming resources. * [Configuration](#configuration) * [Routing](#routing) * [Controllers](#controllers) + * [Rendering](#rendering) * [Models](#models) * [ActiveRecord](#activerecord) * [ActiveRecord Queries](#activerecord-queries) @@ -226,6 +227,37 @@ programming resources. Share no more than two instance variables between a controller and a view. [[link](#shared-instance-variables)] + +### Rendering + +* + Prefer using a template over inline rendering. +[[link](#inline-rendering)] + +```Ruby +# very bad +class ProductsController < ApplicationController + def index + render inline: "<% products.each do |p| %>

<%= p.name %>

<% end %>", type: :erb + end +end + +# good +## app/views/products/index.html.erb +<%= render partial: 'product', collection: products %> + +## app/views/products/_product.html.erb +

<%= product.name %>

+

<%= product.price %>

+ +## app/controllers/foo_controller.rb +class ProductsController < ApplicationController + def index + render :index + end +end +``` + ## Models * From 8c8314a61b1554531d19df1e5cd04be7b3ff7865 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Sun, 27 Dec 2015 21:45:48 +0200 Subject: [PATCH 061/191] Add Rendering plain text tip --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 89a69786..f596a7c5 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,27 @@ class ProductsController < ApplicationController end ``` +* + Prefer `render plain:` over `render text:`. +[[link](#plain-text-rendering)] + +```Ruby +# bad - sets MIME type to `text/html` +... +render text: 'Ruby!' +... + +# bad - requires explicit MIME type declaration +... +render text: 'Ruby!', content_type: 'text/plain' +... + +# good - short and precise +... +render plain: 'Ruby!' +... +``` + ## Models * From 1ba98a7a7bc0940cb8487615a32ffa440f3b207b Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Fri, 25 Dec 2015 16:36:07 +0200 Subject: [PATCH 062/191] Add ActiveSupport extensions usage tip --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index c530d854..89efa3f3 100644 --- a/README.md +++ b/README.md @@ -1016,6 +1016,21 @@ obj&.fly 'the day'.end_with? 'ay' ``` +* + Prefer Ruby's Standard Library over uncommon ActiveSupport extensions.` +[[link](#active_support_extensions)] + +```ruby +# bad +(1..50).to_a.forty_two +1.in? [1, 2] +'day'.in? 'the day' + +# good +(1..50).to_a[41] +[1, 2].include? 1 +'the day'.include? 'day' +``` ## Time From 0d8390e10fa114fedbf1d6d3cea672ae071b5b27 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Fri, 25 Dec 2015 17:12:22 +0200 Subject: [PATCH 063/191] Add ActiveSupport Array#inquiry, Numeric#inquiry, String#inquiry tips --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 89efa3f3..4706e5bd 100644 --- a/README.md +++ b/README.md @@ -1032,6 +1032,35 @@ obj&.fly 'the day'.include? 'day' ``` +* + Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, `Numeric#inquiry` and `String#inquiry`. +[[link](#inquiry)] + +```ruby +# bad - String#inquiry +ruby = 'two'.inquiry +ruby.two? + +# good +ruby = 'two' +ruby == 'two' + +# bad - Array#inquiry +pets = %w(cat dog).inquiry +pets.gopher? + +# good +pets = %w(cat dog).inquiry +pets.include? 'cat' + +# bad - Numeric#inquiry +0.positive? +0.negative? + +# good +0 > 0 +0 < 0 +``` ## Time From 34d3f73cd573f2025710b419a8ed7cde7cd99f40 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 1 Jan 2016 11:30:46 +0000 Subject: [PATCH 064/191] Fix a typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4706e5bd..2ffc781c 100644 --- a/README.md +++ b/README.md @@ -1017,7 +1017,7 @@ obj&.fly ``` * - Prefer Ruby's Standard Library over uncommon ActiveSupport extensions.` + Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. [[link](#active_support_extensions)] ```ruby From 3f5ced47a6fed49b8d686d838fb00fa0f37ce974 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 1 Jan 2016 11:34:58 +0000 Subject: [PATCH 065/191] Drop Ruby 2.3's patch version when referring to it --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 940024e9..9e755851 100644 --- a/README.md +++ b/README.md @@ -1043,7 +1043,7 @@ your application. ## Active Support Core Extensions * - Prefer Ruby 2.3.0's safe navigation operator `&.` over `ActiveSupport#try!`. + Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`. [[link](#try-bang)] ```ruby From 548e8a5ec55957207752fe9f9d217c0875e1a1ee Mon Sep 17 00:00:00 2001 From: Satoshi Ohmori Date: Sun, 14 Feb 2016 15:40:57 +0900 Subject: [PATCH 066/191] Fix typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e755851..c41b10cf 100644 --- a/README.md +++ b/README.md @@ -386,7 +386,7 @@ render plain: 'Ruby!' has_and_belongs_to_many :users end - # prefered way - using has_many :through + # preferred way - using has_many :through class User < ActiveRecord::Base has_many :memberships has_many :groups, through: :memberships @@ -775,7 +775,7 @@ when you need to retrieve a single record by some attributes. end end - # the new prefered way + # the new preferred way class AddNameToPeople < ActiveRecord::Migration def change add_column :people, :name, :string From ac2f308e99e6aa9a8be1af8c2845aae402165353 Mon Sep 17 00:00:00 2001 From: Luciano Sousa Date: Wed, 2 Mar 2016 14:50:18 -0300 Subject: [PATCH 067/191] Add a new bad option in find_by part --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e755851..68eb6916 100644 --- a/README.md +++ b/README.md @@ -655,7 +655,7 @@ when you need to retrieve a single record by id. ``` * - Favor the use of `find_by` over `where` + Favor the use of `find_by` over `where` and `find_by_attribute` when you need to retrieve a single record by some attributes. [[link](#find_by)] @@ -663,6 +663,9 @@ when you need to retrieve a single record by some attributes. # bad User.where(first_name: 'Bruce', last_name: 'Wayne').first + # bad + User.find_by_first_name_and_last_name('Bruce', 'Wayne') + # good User.find_by(first_name: 'Bruce', last_name: 'Wayne') ``` From db108f041729341b5eb94600f371b493aa61f27f Mon Sep 17 00:00:00 2001 From: Harry Kiselev Date: Thu, 10 Mar 2016 17:04:01 +0300 Subject: [PATCH 068/191] Remove duplicated advice for find_each method using. --- README.md | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/README.md b/README.md index 80ad6e3a..1240e58f 100644 --- a/README.md +++ b/README.md @@ -683,23 +683,6 @@ when you need to retrieve a single record by some attributes. User.find_by(first_name: 'Bruce', last_name: 'Wayne') ``` -* - Use `find_each` when you need to process a lot of records. -[[link](#find_each)] - - ```Ruby - # bad - loads all the records at once - # This is very inefficient when the users table has thousands of rows. - User.all.each do |user| - NewsMailer.weekly(user).deliver_now - end - - # good - records are retrieved in batches - User.find_each do |user| - NewsMailer.weekly(user).deliver_now - end - ``` - * Favor the use of `where.not` over SQL. [[link](#where-not)] From 38262118adfce010f5ce0f1704e32474a681344f Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 1 Apr 2016 10:59:26 +0300 Subject: [PATCH 069/191] Replace the mention of Transmuter with Pandoc --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b7a150fe..f6b45035 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ complementary guide to the already existing community-driven Some of the advice here is applicable only to Rails 4.0+. You can generate a PDF or an HTML copy of this guide using -[Transmuter](https://github.com/TechnoGate/transmuter). +[Pandoc](http://pandoc.org/). Translations of the guide are available in the following languages: @@ -356,10 +356,10 @@ render plain: 'Ruby!' attr_accessor :formatted_date_of_birth attr_accessible :login, :first_name, :last_name, :email, :password - + # Rails4+ enums after attr macros, prefer the hash syntax enum gender: { female: 0, male: 1 } - + # followed by association macros belongs_to :country From a1d5f2c36f9768c3effd926a5f636f6eb07e19cd Mon Sep 17 00:00:00 2001 From: Volodymyr Byno Date: Fri, 1 Apr 2016 23:00:11 +0300 Subject: [PATCH 070/191] Use symbols instead of numeric HTTP status codes --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index f6b45035..5a40cbd7 100644 --- a/README.md +++ b/README.md @@ -280,6 +280,22 @@ render plain: 'Ruby!' ... ``` +* + Prefer [corresponding symbols](https://gist.github.com/mlanett/a31c340b132ddefa9cca) to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes. +[[link](#http-status-code-symbols)] + +```Ruby +# bad +... +render status: 500 +... + +# good +... +render status: :forbidden +... +``` + ## Models * From 0d92e65cf62f8b20b8f8a41ef81a02484def43af Mon Sep 17 00:00:00 2001 From: "Harry V. Kiselev" Date: Sat, 2 Apr 2016 08:54:17 +0400 Subject: [PATCH 071/191] Remove unnecessary method call at good example of Array#inquiry section. Remove unnecessary method call at good example of Array#inquiry section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a40cbd7..16ccc353 100644 --- a/README.md +++ b/README.md @@ -1131,7 +1131,7 @@ pets = %w(cat dog).inquiry pets.gopher? # good -pets = %w(cat dog).inquiry +pets = %w(cat dog) pets.include? 'cat' # bad - Numeric#inquiry From 96b59ebf41de52e9079be3c74340974a45d4da8f Mon Sep 17 00:00:00 2001 From: Michael Lutsiuk Date: Mon, 30 May 2016 12:04:22 +0300 Subject: [PATCH 072/191] Fix a typo - persistance -> persistence (#185) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16ccc353..a1943cb8 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ render status: :forbidden 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-persistance + place is in helpers. Keep your models for business logic and data-persistence only. [[link](#model-business-logic)] From 7321c4987c38299b192718a4b6e507b051a86ad2 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Wed, 6 Jul 2016 10:30:33 +0300 Subject: [PATCH 073/191] Add Migrations Foreign Key naming style (#189) --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index a1943cb8..40c7724e 100644 --- a/README.md +++ b/README.md @@ -818,6 +818,28 @@ when you need to retrieve a single record by some attributes. stop, because of changes in the models used. [[link](#no-model-class-migrations)] +* + Name your foreign keys explicitly instead of relying on Rails auto-generated + FK names. (http://edgeguides.rubyonrails.org/active_record_migrations.html#foreign-keys) + + ```Ruby + # bad + class AddFkArticlesToAuthors < ActiveRecord::Migration + def change + add_foreign_key :articles, :authors + end + end + + # good + class AddFkArticlesToAuthors < ActiveRecord::Migration + def change + add_foreign_key :articles, :authors, name: :articles_author_id_fk + end + end + ``` + +[[link](#meaningful-foreign-key-naming)] + ## Views * From 16139f88c066a70350eb63876a363cfe61fe9936 Mon Sep 17 00:00:00 2001 From: Quinn Harris Date: Fri, 15 Jul 2016 02:59:12 -0500 Subject: [PATCH 074/191] Add ActiveRecord save bang guidance (#190) --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 40c7724e..934bda24 100644 --- a/README.md +++ b/README.md @@ -649,6 +649,38 @@ render status: :forbidden end ``` +* + When persisting AR objects always use the exception raising bang! method or handle the method return value. + This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. +[[link](#save-bang)] + + ```Ruby + # bad + user.create(name: 'Bruce') + + # bad + user.save + + # good + user.create!(name: 'Bruce') + # or + bruce = user.create(name: 'Bruce') + if bruce.persisted? + ... + else + ... + end + + # good + user.save! + # or + if user.save + ... + else + ... + end + ``` + ### ActiveRecord Queries * From 5d1b0a9da4999899db60d4db424e1768ec33c6d6 Mon Sep 17 00:00:00 2001 From: Cao Quang Binh Date: Fri, 15 Jul 2016 14:59:53 +0700 Subject: [PATCH 075/191] [Add] Vietnamese version (#187) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 934bda24..43d6b25c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Translations of the guide are available in the following languages: * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) * [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md) +* [Vietnamese](https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md) # The Rails Style Guide From 56037293fcf13042951ce2b9f1637129d9d1fb7b Mon Sep 17 00:00:00 2001 From: Aaron Wolfson Date: Sat, 23 Jul 2016 02:05:36 -0500 Subject: [PATCH 076/191] Add Rails 4 Test Prescriptions book reference (#192) This book belongs in the "Further Reading" section as an equal of the other choices. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 43d6b25c..4c0a29fd 100644 --- a/README.md +++ b/README.md @@ -1298,6 +1298,7 @@ you have time to spare: * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book) * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book) * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) +* [Rails 4 Test Prescriptions](https://pragprog.com/book/nrtest2/rails-4-test-prescriptions) * [Better Specs for RSpec](http://betterspecs.org) # Contributing From b16714c01a97af5b4597b560f3aa47c4e32ef59c Mon Sep 17 00:00:00 2001 From: Vadim Stroganov Date: Sat, 23 Jul 2016 10:06:30 +0300 Subject: [PATCH 077/191] Fix a typo (#191) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4c0a29fd..311c6df3 100644 --- a/README.md +++ b/README.md @@ -651,7 +651,7 @@ render status: :forbidden ``` * - When persisting AR objects always use the exception raising bang! method or handle the method return value. + When persisting AR objects always use the exception raising bang! method or handle the method return value. This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. [[link](#save-bang)] @@ -765,7 +765,7 @@ when you need to retrieve a single record by some attributes. [[link](#squished-heredocs)] ```Ruby - User.find_by_sql(< Date: Sun, 23 Oct 2016 21:40:00 +0300 Subject: [PATCH 078/191] Fix misplaced link --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 311c6df3..3a6255cf 100644 --- a/README.md +++ b/README.md @@ -854,6 +854,7 @@ when you need to retrieve a single record by some attributes. * Name your foreign keys explicitly instead of relying on Rails auto-generated FK names. (http://edgeguides.rubyonrails.org/active_record_migrations.html#foreign-keys) +[[link](#meaningful-foreign-key-naming)] ```Ruby # bad @@ -871,8 +872,6 @@ when you need to retrieve a single record by some attributes. end ``` -[[link](#meaningful-foreign-key-naming)] - ## Views * From eaa7a7ef14938689b217d5e486e00044f133dcd9 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Thu, 10 Nov 2016 01:05:08 +0200 Subject: [PATCH 079/191] Add good/bad comparison to default attribute values (#203) --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3a6255cf..e632d773 100644 --- a/README.md +++ b/README.md @@ -803,8 +803,17 @@ when you need to retrieve a single record by some attributes. ```Ruby # bad - application enforced default value - def amount - self[:amount] or 0 + class Product < ActiveRecord::Base + def amount + self[:amount] || 0 + end + end + + # good - database enforced + class AddDefaultAmountToProducts < ActiveRecord::Migration + def change + change_column :products, :amount, :integer, default: 0 + end end ``` From d4de68a5f7c0df565ef106916353d2ec5ee34c92 Mon Sep 17 00:00:00 2001 From: Jonathan Chen Date: Sat, 12 Nov 2016 08:51:21 -0500 Subject: [PATCH 080/191] Point URL to stable documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e632d773..e39a60c5 100644 --- a/README.md +++ b/README.md @@ -862,7 +862,7 @@ when you need to retrieve a single record by some attributes. * Name your foreign keys explicitly instead of relying on Rails auto-generated - FK names. (http://edgeguides.rubyonrails.org/active_record_migrations.html#foreign-keys) + FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) [[link](#meaningful-foreign-key-naming)] ```Ruby From 38d518b6abfea629b1feac41786cb5499c1faec5 Mon Sep 17 00:00:00 2001 From: Oleg Yakovenko Date: Wed, 23 Nov 2016 16:43:27 +0200 Subject: [PATCH 081/191] Change column default value by more appropriate method --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e39a60c5..e09a6cbe 100644 --- a/README.md +++ b/README.md @@ -812,7 +812,7 @@ when you need to retrieve a single record by some attributes. # good - database enforced class AddDefaultAmountToProducts < ActiveRecord::Migration def change - change_column :products, :amount, :integer, default: 0 + change_column_default :products, :amount, 0 end end ``` From 37f10f351e34f433152b18c00bce384897b2d6f4 Mon Sep 17 00:00:00 2001 From: Anton Antonov Date: Sun, 23 Oct 2016 21:58:52 +0300 Subject: [PATCH 082/191] Add rule for models in migrations (#143) --- README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e09a6cbe..f3024b4a 100644 --- a/README.md +++ b/README.md @@ -854,11 +854,64 @@ when you need to retrieve a single record by some attributes. end ``` -* - Don't use model classes in migrations. The model classes are constantly - evolving and at some point in the future migrations that used to work might - stop, because of changes in the models used. -[[link](#no-model-class-migrations)] +* + 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 +[[link](#define-model-class-migrations)] + + ```Ruby + # db/migrate/.rb + # frozen_string_literal: true + + # bad + class ModifyDefaultStatusForProducts < ActiveRecord::Migration + def change + old_status = 'pending_manual_approval' + new_status = 'pending_approval' + + reversible do |dir| + dir.up do + Product.where(status: old_status).update_all(status: new_status) + change_column :products, :status, :string, default: new_status + end + + dir.down do + Product.where(status: new_status).update_all(status: old_status) + change_column :products, :status, :string, default: old_status + end + end + end + end + + # 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. + class MigrationProduct < ActiveRecord::Base + self.table_name = :products + end + + class ModifyDefaultStatusForProducts < ActiveRecord::Migration + def change + old_status = 'pending_manual_approval' + new_status = 'pending_approval' + + reversible do |dir| + dir.up do + MigrationProduct.where(status: old_status).update_all(status: new_status) + change_column :products, :status, :string, default: new_status + end + + dir.down do + MigrationProduct.where(status: new_status).update_all(status: old_status) + change_column :products, :status, :string, default: old_status + end + end + end + end + ``` * Name your foreign keys explicitly instead of relying on Rails auto-generated From 225258e8071139f7413b2725c7dc70f94d9cf487 Mon Sep 17 00:00:00 2001 From: Rahul Chanila Date: Tue, 27 Dec 2016 10:57:13 +0530 Subject: [PATCH 083/191] Add rule to avoid using model methods which skip validation --- README.md | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f3024b4a..c1a408a5 100644 --- a/README.md +++ b/README.md @@ -537,12 +537,29 @@ render status: :forbidden end ``` -* +* Beware of the behavior of the - [`update_attribute`](http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute) - method. It doesn't run the model validations (unlike `update_attributes`) and + [following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations) + methods. They do not run the model validations and could easily corrupt the model state. -[[link](#beware-update-attribute)] +[[link](#beware-skip-model-validations)] + + ```Ruby + # bad + Article.first.decrement!(:view_count) + DiscussionBoard.decrement_counter(:post_count, 5) + Article.first.increment!(:view_count) + DiscussionBoard.increment_counter(:post_count, 5) + person.toggle :active + product.touch + Billing.update_all("category = 'authorized', author = 'David'") + user.update_attribute(website: 'example.com') + user.update_columns(last_request_at: Time.current) + Post.update_counters 5, comment_count: -1, action_count: 1 + + # good + user.update_attributes(website: 'example.com') + ``` * Use user-friendly URLs. Show some descriptive attribute of the model in the URL From 3a04772d6d3a96c5e98d2a4170b4b38b8339b021 Mon Sep 17 00:00:00 2001 From: sue445 Date: Fri, 6 Jan 2017 00:01:02 +0900 Subject: [PATCH 084/191] Add rule of reversible migration --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index c1a408a5..ef14564c 100644 --- a/README.md +++ b/README.md @@ -951,6 +951,45 @@ when you need to retrieve a single record by some attributes. end ``` +* + Don't use non-reversible migration commands in the `change` method. + Reversible migration commands are listed below. + [ActiveRecord::Migration::CommandRecorder](http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html) +[[link](#reversible-migration)] + + ```ruby + # bad + class DropUsers < ActiveRecord::Migration + def change + drop_table :users + end + end + + # good + class DropUsers < ActiveRecord::Migration + def up + drop_table :users + end + + def down + create_table :users do |t| + t.string :name + end + end + end + + # good + # In this case, block will be used by create_table in rollback + # http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table + class DropUsers < ActiveRecord::Migration + def change + drop_table :users do |t| + t.string :name + end + end + end + ``` + ## Views * From 43ea154efba6fb038cedb102f6c3cc27f3254d81 Mon Sep 17 00:00:00 2001 From: marocchino Date: Thu, 19 Jan 2017 13:52:30 +0900 Subject: [PATCH 085/191] Fix example code (#212) See http://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_attribute --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef14564c..ae5ce735 100644 --- a/README.md +++ b/README.md @@ -553,7 +553,7 @@ render status: :forbidden person.toggle :active product.touch Billing.update_all("category = 'authorized', author = 'David'") - user.update_attribute(website: 'example.com') + user.update_attribute(:website, 'example.com') user.update_columns(last_request_at: Time.current) Post.update_counters 5, comment_count: -1, action_count: 1 From c08b66f76649a9d28a45fe6486a7d532672aabe8 Mon Sep 17 00:00:00 2001 From: Ammar Alammar Date: Sun, 30 Apr 2017 19:40:57 +0300 Subject: [PATCH 086/191] Lowercase code blocks language identifier This will allow Rouge to syntax highlight the code examples which helps with hosting this guide on Github Pages. --- README.md | 106 +++++++++++++++++++++++++++--------------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index ae5ce735..1f1916eb 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ programming resources. * Mark additional assets for precompilation (if any): - ```Ruby + ```ruby # config/environments/production.rb # Precompile additional assets (application.js, application.css, #and all non-JS/CSS are already added) @@ -103,7 +103,7 @@ programming resources. Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method: - ```Ruby + ```ruby Rails::Application.config_for(:yaml_file) ``` @@ -114,7 +114,7 @@ programming resources. them at all?) use `member` and `collection` routes. [[link](#member-collection-routes)] - ```Ruby + ```ruby # bad get 'subscriptions/:id/unsubscribe' resources :subscriptions @@ -139,7 +139,7 @@ programming resources. alternative block syntax. [[link](#many-member-collection-routes)] - ```Ruby + ```ruby resources :subscriptions do member do get 'unsubscribe' @@ -160,7 +160,7 @@ programming resources. models. [[link](#nested-routes)] - ```Ruby + ```ruby class Post < ActiveRecord::Base has_many :comments end @@ -178,7 +178,7 @@ programming resources. * If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`. - ```Ruby + ```ruby resources :posts, shallow: true do resources :comments do resources :versions @@ -190,7 +190,7 @@ programming resources. Use namespaced routes to group related actions. [[link](#namespaced-routes)] - ```Ruby + ```ruby namespace :admin do # Directs /admin/products/* to Admin::ProductsController # (app/controllers/admin/products_controller.rb) @@ -203,7 +203,7 @@ programming resources. in every controller accessible via GET requests. [[link](#no-wild-routes)] - ```Ruby + ```ruby # very bad match ':controller(/:action(/:id(.:format)))' ``` @@ -236,7 +236,7 @@ programming resources. Prefer using a template over inline rendering. [[link](#inline-rendering)] -```Ruby +```ruby # very bad class ProductsController < ApplicationController def index @@ -264,7 +264,7 @@ end Prefer `render plain:` over `render text:`. [[link](#plain-text-rendering)] -```Ruby +```ruby # bad - sets MIME type to `text/html` ... render text: 'Ruby!' @@ -285,7 +285,7 @@ render plain: 'Ruby!' Prefer [corresponding symbols](https://gist.github.com/mlanett/a31c340b132ddefa9cca) to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes. [[link](#http-status-code-symbols)] -```Ruby +```ruby # bad ... render status: 500 @@ -313,7 +313,7 @@ render status: :forbidden [ActiveAttr](https://github.com/cgriego/active_attr) gem. [[link](#activeattr-gem)] - ```Ruby + ```ruby class Message include ActiveAttr::Model @@ -348,7 +348,7 @@ render status: :forbidden you have a very good reason (like a database that's not under your control). [[link](#keep-ar-defaults)] - ```Ruby + ```ruby # bad - don't do this if you can modify the schema class Transaction < ActiveRecord::Base self.table_name = 'order' @@ -361,7 +361,7 @@ render status: :forbidden the class definition. [[link](#macro-style-methods)] - ```Ruby + ```ruby class User < ActiveRecord::Base # keep the default scope first (if any) default_scope { where(active: true) } @@ -404,7 +404,7 @@ render status: :forbidden :through` allows additional attributes and validations on the join model. [[link](#has-many-through)] - ```Ruby + ```ruby # not so good - using has_and_belongs_to_many class User < ActiveRecord::Base has_and_belongs_to_many :groups @@ -435,7 +435,7 @@ render status: :forbidden Prefer `self[:attribute]` over `read_attribute(:attribute)`. [[link](#read-attribute)] - ```Ruby + ```ruby # bad def amount read_attribute(:amount) * 100 @@ -451,7 +451,7 @@ render status: :forbidden Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. [[link](#write-attribute)] - ```Ruby + ```ruby # bad def amount write_attribute(:amount, 100) @@ -468,7 +468,7 @@ render status: :forbidden validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). [[link](#sexy-validations)] - ```Ruby + ```ruby # bad validates_presence_of :email validates_length_of :email, maximum: 100 @@ -482,7 +482,7 @@ render status: :forbidden regular expression mapping, create a custom validator file. [[link](#custom-validator-file)] - ```Ruby + ```ruby # bad class Person validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } @@ -513,7 +513,7 @@ render status: :forbidden Use named scopes freely. [[link](#named-scopes)] - ```Ruby + ```ruby class User < ActiveRecord::Base scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } @@ -529,7 +529,7 @@ render status: :forbidden object. Arguably you can define even simpler scopes like this. [[link](#named-scope-class)] - ```Ruby + ```ruby class User < ActiveRecord::Base def self.with_orders joins(:orders).select('distinct(users.id)') @@ -544,7 +544,7 @@ render status: :forbidden could easily corrupt the model state. [[link](#beware-skip-model-validations)] - ```Ruby + ```ruby # bad Article.first.decrement!(:view_count) DiscussionBoard.decrement_counter(:post_count, 5) @@ -571,7 +571,7 @@ render status: :forbidden the `id` of the record as a String. It could be overridden to include another human-readable attribute. - ```Ruby + ```ruby class Person def to_param "#{id} #{name}".parameterize @@ -586,7 +586,7 @@ render status: :forbidden * Use the `friendly_id` gem. It allows creation of human-readable URLs by using some descriptive attribute of the model instead of its `id`. - ```Ruby + ```ruby class Person extend FriendlyId friendly_id :name, use: :slugged @@ -605,7 +605,7 @@ render status: :forbidden [[link](#find-each)] - ```Ruby + ```ruby # bad Person.all.each do |person| person.do_awesome_stuff @@ -631,7 +631,7 @@ render status: :forbidden `before_destroy` callbacks that perform validation with `prepend: true`. [[link](#before_destroy)] - ```Ruby + ```ruby # bad (roles will be deleted automatically even if super_admin? is true) has_many :roles, dependent: :destroy @@ -655,7 +655,7 @@ render status: :forbidden Define the `dependent` option to the `has_many` and `has_one` associations. [[link](#has_many-has_one-dependent-option)] - ```Ruby + ```ruby # bad class Post < ActiveRecord::Base has_many :comments @@ -672,7 +672,7 @@ render status: :forbidden This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. [[link](#save-bang)] - ```Ruby + ```ruby # bad user.create(name: 'Bruce') @@ -707,7 +707,7 @@ render status: :forbidden attacks. [[link](#avoid-interpolation)] - ```Ruby + ```ruby # bad - param will be interpolated unescaped Client.where("orders_count = #{params[:orders]}") @@ -720,7 +720,7 @@ render status: :forbidden when you have more than 1 placeholder in your query. [[link](#named-placeholder)] - ```Ruby + ```ruby # okish Client.where( 'created_at >= ? AND created_at <= ?', @@ -739,7 +739,7 @@ render status: :forbidden when you need to retrieve a single record by id. [[link](#find)] - ```Ruby + ```ruby # bad User.where(id: id).take @@ -752,7 +752,7 @@ when you need to retrieve a single record by id. when you need to retrieve a single record by some attributes. [[link](#find_by)] - ```Ruby + ```ruby # bad User.where(first_name: 'Bruce', last_name: 'Wayne').first @@ -767,7 +767,7 @@ when you need to retrieve a single record by some attributes. Favor the use of `where.not` over SQL. [[link](#where-not)] - ```Ruby + ```ruby # bad User.where("id != ?", id) @@ -781,7 +781,7 @@ when you need to retrieve a single record by some attributes. tools (including GitHub, Atom, and RubyMine). [[link](#squished-heredocs)] - ```Ruby + ```ruby User.find_by_sql(<<-SQL.squish) SELECT users.id, accounts.plan @@ -818,7 +818,7 @@ when you need to retrieve a single record by some attributes. application layer. [[link](#default-migration-values)] - ```Ruby + ```ruby # bad - application enforced default value class Product < ActiveRecord::Base def amount @@ -851,7 +851,7 @@ when you need to retrieve a single record by some attributes. use the `change` method instead of `up` and `down` methods. [[link](#change-vs-up-down)] - ```Ruby + ```ruby # the old way class AddNameToPeople < ActiveRecord::Migration def up @@ -876,7 +876,7 @@ when you need to retrieve a single record by some attributes. so that you don't end up with broken migrations in the future [[link](#define-model-class-migrations)] - ```Ruby + ```ruby # db/migrate/.rb # frozen_string_literal: true @@ -935,7 +935,7 @@ when you need to retrieve a single record by some attributes. FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) [[link](#meaningful-foreign-key-naming)] - ```Ruby + ```ruby # bad class AddFkArticlesToAuthors < ActiveRecord::Migration def change @@ -1043,7 +1043,7 @@ when you need to retrieve a single record by some attributes. these directories must be described in the `application.rb` file in order to be loaded. - ```Ruby + ```ruby # config/application.rb config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] ``` @@ -1073,7 +1073,7 @@ when you need to retrieve a single record by some attributes. The value for `users.show.title` can be looked up in the template `app/views/users/show.html.haml` like this: - ```Ruby + ```ruby = t '.title' ``` @@ -1083,7 +1083,7 @@ when you need to retrieve a single record by some attributes. hierarchy. [[link](#dot-separated-keys)] - ```Ruby + ```ruby # bad I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] @@ -1141,7 +1141,7 @@ your application. The errors are disabled by default. [[link](#enable-delivery-errors)] - ```Ruby + ```ruby # config/environments/development.rb config.action_mailer.raise_delivery_errors = true @@ -1153,7 +1153,7 @@ your application. environment. [[link](#local-smtp)] - ```Ruby + ```ruby # config/environments/development.rb config.action_mailer.smtp_settings = { @@ -1167,7 +1167,7 @@ your application. Provide default settings for the host name. [[link](#default-hostname)] - ```Ruby + ```ruby # config/environments/development.rb config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } @@ -1184,7 +1184,7 @@ your application. methods don't. [[link](#url-not-path-in-email)] - ```Ruby + ```ruby # bad You can always find more info about this course <%= link_to 'here', course_path(@course) %> @@ -1198,7 +1198,7 @@ your application. Format the from and to addresses properly. Use the following format: [[link](#email-addresses)] - ```Ruby + ```ruby # in your mailer class default from: 'Your Name ' ``` @@ -1208,7 +1208,7 @@ your application. `test`: [[link](#delivery-method-test)] - ```Ruby + ```ruby # config/environments/test.rb config.action_mailer.delivery_method = :test @@ -1218,7 +1218,7 @@ your application. The delivery method for development and production should be `smtp`: [[link](#delivery-method-smtp)] - ```Ruby + ```ruby # config/environments/development.rb, config/environments/production.rb config.action_mailer.delivery_method = :smtp @@ -1321,7 +1321,7 @@ pets.include? 'cat' Config your timezone accordingly in `application.rb`. [[link](#tz-config)] - ```Ruby + ```ruby config.time_zone = 'Eastern European Time' # optional - note it can be only :utc or :local (default is :utc) config.active_record.default_timezone = :local @@ -1331,7 +1331,7 @@ pets.include? 'cat' Don't use `Time.parse`. [[link](#time-parse)] - ```Ruby + ```ruby # bad Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone. @@ -1343,7 +1343,7 @@ pets.include? 'cat' Don't use `Time.now`. [[link](#time-now)] - ```Ruby + ```ruby # bad Time.now # => Returns system time and ignores your configured time zone. @@ -1372,7 +1372,7 @@ pets.include? 'cat' all Linux specific gems to a `linux` group: [[link](#os-specific-gemfile-locks)] - ```Ruby + ```ruby # Gemfile group :darwin do gem 'rb-fsevent' @@ -1387,7 +1387,7 @@ pets.include? 'cat' To require the appropriate gems in the right environment, add the following to `config/application.rb`: - ```Ruby + ```ruby platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym Bundler.require(platform) ``` From da3a6772fabb5d096197115cbc47a6ff483bba57 Mon Sep 17 00:00:00 2001 From: Andrei Beliankou Date: Mon, 29 May 2017 18:55:47 +0200 Subject: [PATCH 087/191] Removed the unfinished German translation. This translation will stay unfinished since I have no time for this project. Honestly removed :) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 1f1916eb..73cb0d18 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ Translations of the guide are available in the following languages: * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) -* [German](https://github.com/arbox/de-rails-style-guide/blob/master/README-deDE.md) * [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) From 6494b4bc09f51f7c6781abedf96f478dc4291aba Mon Sep 17 00:00:00 2001 From: wata_mac Date: Sat, 9 Dec 2017 21:01:59 +0900 Subject: [PATCH 088/191] Add rule about lexically scoped action filter --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 73cb0d18..8e57a5d0 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,24 @@ programming resources. Share no more than two instance variables between a controller and a view. [[link](#shared-instance-variables)] +* + 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. +[[link](#lexically-scoped-action-filter)] + +```ruby +# bad +class UsersController < ApplicationController + before_action :require_login, only: :export +end + +# good +class UsersController < ApplicationController + before_action :require_login, only: :export + + def export + end +end +``` ### Rendering From 238410b04089d8dcc78c292c6a08a1e0e5445f18 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 4 Apr 2018 15:26:57 -0400 Subject: [PATCH 089/191] Remove Numeric#inquiry as bad This has been upstreamed in Ruby 2.3: https://ruby-doc.org/core-2.3.0/Numeric.html#method-i-negative-3F It also allows writing `(-2..2).select(&:positive?)` instead of `(-2..2).select { |i| i > 0 }` which is in line with https://github.com/bbatsov/ruby-style-guide#single-action-blocks --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 8e57a5d0..d2652aaa 100644 --- a/README.md +++ b/README.md @@ -1303,7 +1303,7 @@ obj&.fly ``` * - Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, `Numeric#inquiry` and `String#inquiry`. + Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, and `String#inquiry`. [[link](#inquiry)] ```ruby @@ -1322,14 +1322,6 @@ pets.gopher? # good pets = %w(cat dog) pets.include? 'cat' - -# bad - Numeric#inquiry -0.positive? -0.negative? - -# good -0 > 0 -0 < 0 ``` ## Time From 373f5c42aa220f5b8b8c8eab6ac5d740f3b2b687 Mon Sep 17 00:00:00 2001 From: Bart Date: Wed, 4 Apr 2018 15:15:30 -0400 Subject: [PATCH 090/191] Prefer raise over fail Keep consistency with https://github.com/bbatsov/ruby-style-guide#prefer-raise-over-fail --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d2652aaa..e509bd7d 100644 --- a/README.md +++ b/README.md @@ -655,7 +655,7 @@ render status: :forbidden before_destroy :ensure_deletable def ensure_deletable - fail "Cannot delete super admin." if super_admin? + raise "Cannot delete super admin." if super_admin? end # good @@ -664,7 +664,7 @@ render status: :forbidden before_destroy :ensure_deletable, prepend: true def ensure_deletable - fail "Cannot delete super admin." if super_admin? + raise "Cannot delete super admin." if super_admin? end ``` From 7a17a4500cf5d7ef215d37ae64638361b3a8ffa0 Mon Sep 17 00:00:00 2001 From: Ted Johansson Date: Mon, 11 Sep 2017 17:28:57 +0200 Subject: [PATCH 091/191] Add recommendation to not order by `id` 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 instead. --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index e509bd7d..3ff2b366 100644 --- a/README.md +++ b/README.md @@ -791,6 +791,22 @@ when you need to retrieve a single record by some attributes. # good User.where.not(id: id) ``` + +* + Don't use the `id` 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. +[[link](#order-by-id)] + + ```ruby + # bad + scope :chronological, -> { order(id: :asc) } + + # good + scope :chronological, -> { order(created_at: :asc) } + ``` + * When specifying an explicit query in a method such as `find_by_sql`, use heredocs with `squish`. This allows you to legibly format the SQL with From 742cc199f812121e392b000bc11699521fc651cb Mon Sep 17 00:00:00 2001 From: Stefan Wrobel Date: Tue, 20 Mar 2018 22:07:56 -0700 Subject: [PATCH 092/191] Add size/length vs count rule --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 3ff2b366..74516f75 100644 --- a/README.md +++ b/README.md @@ -835,6 +835,21 @@ when you need to retrieve a single record by some attributes. SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n acounts\n ON\n accounts.user_id = users.id ``` +* + When querying ActiveRecord collections, prefer `size` (selects between count/length behavior based on whether collection is already loaded) or `length` (always loads the whole collection and counts the array elements) over `count` (always does a database query for the count). +[[link](#size-over-count-or-length)] + + ```ruby + # bad + User.count + + # good + User.all.size + + # good - if you really need to load all users into memory + User.all.length + ``` + ## Migrations * From 6c977ec9c8ba746a73177aa2b76be947b7ba4d8a Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Fri, 1 Jun 2018 11:54:42 +0900 Subject: [PATCH 093/191] Update a few links --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 74516f75..842aaa70 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The goal of this guide is to present a set of best practices and style prescriptions for Ruby on Rails 4 development. It's a complementary guide to the already existing community-driven -[Ruby coding style guide](https://github.com/bbatsov/ruby-style-guide). +[Ruby coding style guide](https://github.com/rubocop-hq/ruby-style-guide). Some of the advice here is applicable only to Rails 4.0+. @@ -1468,13 +1468,11 @@ 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 via -[gittip](https://www.gittip.com/bbatsov). - -[![Support via Gittip](https://rawgithub.com/twolfson/gittip-badge/0.2.0/dist/gittip.png)](https://www.gittip.com/bbatsov) +[Patreon](https://www.patreon.com/bbatsov). ## How to Contribute? -It's easy, just follow the [contribution guidelines](https://github.com/bbatsov/rails-style-guide/blob/master/CONTRIBUTING.md). +It's easy, just follow the [contribution guidelines](https://github.com/rubocop-hq/rails-style-guide/blob/master/CONTRIBUTING.md). # License From 68b111b9a86181f0ad9cae843b87e68f02de060d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abra=C3=A3o=20Miranda?= Date: Fri, 8 Jun 2018 05:06:59 -0300 Subject: [PATCH 094/191] Add Brasilian Portuguese translation (#200) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 842aaa70..f3e35d9e 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Translations of the guide are available in the following languages: * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) * [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md) * [Vietnamese](https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md) +* [Portuguese (pt-BR)](https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md) # The Rails Style Guide From 9acffadd2ab054fb6bb2282221db0d244a66816e Mon Sep 17 00:00:00 2001 From: Alexander Popov Date: Tue, 29 May 2018 13:54:53 +0300 Subject: [PATCH 095/191] Add guideline for ordering of ActiveRecord callback declarations --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index f3e35d9e..1272411b 100644 --- a/README.md +++ b/README.md @@ -555,6 +555,45 @@ render status: :forbidden end ``` +* + Order callback declarations in the order, in which they will be executed. For + referenece, see [Available Callbacks](http://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks) +[[link](#callbacks-order)] + + ```Ruby + #bad + class Person + after_commit/after_rollback :after_commit_callback + after_save :after_save_callback + around_save :around_save_callback + after_update :after_update_callback + before_update :before_update_callback + after_validation :after_validation_callback + before_validation :before_validation_callback + before_save :before_save_callback + before_create :before_create_callback + after_create :after_create_callback + around_create :around_create_callback + around_update :around_update_callback + end + + #good + class Person + before_validation :before_validation_callback + after_validation :after_validation_callback + before_save :before_save_callback + around_save :around_save_callback + before_create :before_create_callback + around_create :around_create_callback + after_create :after_create_callback + before_update :before_update_callback + around_update :around_update_callback + after_update :after_update_callback + after_save :after_save_callback + after_commit/after_rollback :after_commit_callback + end + ``` + * Beware of the behavior of the [following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations) From 0c1390b6a82d92728e3f209f0c8fb2f672e8da14 Mon Sep 17 00:00:00 2001 From: Michael Reinsch Date: Fri, 8 Jun 2018 10:09:39 +0200 Subject: [PATCH 096/191] Add a rule about single attribute validations (#227) --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 1272411b..e879ffaf 100644 --- a/README.md +++ b/README.md @@ -495,6 +495,21 @@ render status: :forbidden validates :email, presence: true, length: { maximum: 100 } ``` +* + To make validations easy to read, don't list multiple attributes per + validation +[[link](#single-attribute-validations)] + + ```ruby + # bad + validates :email, :password, presence: true + validates :email, length: { maximum: 100 } + + # good + validates :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. From 8bda49a7639a45a8267dc4c701d0be81ffeec895 Mon Sep 17 00:00:00 2001 From: Will Jordan Date: Wed, 18 Oct 2017 09:22:07 -0700 Subject: [PATCH 097/191] Clarify find_by and find_by rules Add more specific description and examples to clarify `find` and `find_by` rules. --- README.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e879ffaf..525777e7 100644 --- a/README.md +++ b/README.md @@ -807,31 +807,43 @@ render status: :forbidden ``` * - Favor the use of `find` over `where` -when you need to retrieve a single record by id. + Favor the use of `find` over `where.take!`, `find_by!`, and `find_by_id!` + when you need to retrieve a single record by primary key id and raise + `ActiveRecord::RecordNotFound` when the record is not found. [[link](#find)] ```ruby # bad - User.where(id: id).take + User.where(id: id).take! + + # bad + User.find_by_id!(id) + # bad + User.find_by!(id: id) + # good User.find(id) ``` * - Favor the use of `find_by` over `where` and `find_by_attribute` -when you need to retrieve a single record by some attributes. + Favor the use of `find_by` over `where.take` and `find_by_attribute` + when you need to retrieve a single record by one or more attributes and return + `nil` when the record is not found. [[link](#find_by)] ```ruby # bad - User.where(first_name: 'Bruce', last_name: 'Wayne').first + User.where(id: id).take + User.where(first_name: 'Bruce', last_name: 'Wayne').take # bad + User.find_by_id(id) + # bad, deprecated in ActiveRecord 4.0, removed in 4.1+ User.find_by_first_name_and_last_name('Bruce', 'Wayne') # good + User.find_by(id: id) User.find_by(first_name: 'Bruce', last_name: 'Wayne') ``` From ec2b1c02e3b1a34ad4cfff66c3062b594225b817 Mon Sep 17 00:00:00 2001 From: Alex Popov Date: Fri, 5 Aug 2016 15:36:02 +0300 Subject: [PATCH 098/191] Match good and bad examples for status code guideline --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 525777e7..a934e8f7 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ render plain: 'Ruby!' ```ruby # bad ... -render status: 500 +render status: 403 ... # good From 8e6da17a7c154c162d474674cda658ea97c7f7aa Mon Sep 17 00:00:00 2001 From: Alex Popov Date: Thu, 11 Aug 2016 11:53:54 +0300 Subject: [PATCH 099/191] Favor Rails built-in ids method over pluck(:id) --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index a934e8f7..4b54d453 100644 --- a/README.md +++ b/README.md @@ -874,6 +874,18 @@ render status: :forbidden scope :chronological, -> { order(created_at: :asc) } ``` +* + Favor the use of `ids` over `pluck(:id)`. +[[link](#ids)] + + ```Ruby + # bad + User.pluck(:id) + + # good + User.ids + ``` + * When specifying an explicit query in a method such as `find_by_sql`, use heredocs with `squish`. This allows you to legibly format the SQL with From 92e64605a9e8d5f22b119c7bf9a49b30efe3ac5e Mon Sep 17 00:00:00 2001 From: Eugene Gladyshev Date: Fri, 3 Aug 2018 17:38:01 +0300 Subject: [PATCH 100/191] Fix name of model --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b54d453..e4d8b786 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ programming resources. has_many :comments end - class Comments < ActiveRecord::Base + class Comment < ActiveRecord::Base belongs_to :post end From ad4385aeb7f9883f9367280f7e8e63ceb771da17 Mon Sep 17 00:00:00 2001 From: Jonas Peschla Date: Wed, 19 Sep 2018 22:48:58 +0200 Subject: [PATCH 101/191] Add section about String#to_time (#234) --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index e4d8b786..13f3eb2f 100644 --- a/README.md +++ b/README.md @@ -1458,6 +1458,18 @@ pets.include? 'cat' Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 ``` +* + Don't use [`String#to_time`](https://apidock.com/rails/String/to_time) +[[link](#to-time)] + + ```ruby + # bad - assumes time string given is in the system's time zone. + '2015-03-02 19:05:37'.to_time + + # good + Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 + ``` + * Don't use `Time.now`. [[link](#time-now)] From f268526d02459c03ec07dc067b79ce3292d745b2 Mon Sep 17 00:00:00 2001 From: Johan Date: Thu, 20 Sep 2018 17:50:09 +0200 Subject: [PATCH 102/191] Update markdown indention from 2 to 4 spaces (#235) Seems everything has to be indented by 4 spaces (or a single tab) to be certain the markup will be properly processed by all markdown tools. See http://brettterpstra.com/2015/08/24/write-better-markdown/#indentation for details. --- README.md | 2380 ++++++++++++++++++++++++++--------------------------- 1 file changed, 1190 insertions(+), 1190 deletions(-) diff --git a/README.md b/README.md index 13f3eb2f..a3173eda 100644 --- a/README.md +++ b/README.md @@ -15,14 +15,14 @@ You can generate a PDF or an HTML copy of this guide using Translations of the guide are available in the following languages: -* [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) -* [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) -* [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) -* [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) -* [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) -* [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md) -* [Vietnamese](https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md) -* [Portuguese (pt-BR)](https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md) + * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) + * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) + * [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) + * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) + * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) + * [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md) + * [Vietnamese](https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md) + * [Portuguese (pt-BR)](https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md) # The Rails Style Guide @@ -44,194 +44,194 @@ programming resources. ## Table of Contents -* [Configuration](#configuration) -* [Routing](#routing) -* [Controllers](#controllers) - * [Rendering](#rendering) -* [Models](#models) - * [ActiveRecord](#activerecord) - * [ActiveRecord Queries](#activerecord-queries) -* [Migrations](#migrations) -* [Views](#views) -* [Internationalization](#internationalization) -* [Assets](#assets) -* [Mailers](#mailers) -* [Active Support Core Extensions](#active-support-core-extensions) -* [Time](#time) -* [Bundler](#bundler) -* [Managing processes](#managing-processes) + * [Configuration](#configuration) + * [Routing](#routing) + * [Controllers](#controllers) + * [Rendering](#rendering) + * [Models](#models) + * [ActiveRecord](#activerecord) + * [ActiveRecord Queries](#activerecord-queries) + * [Migrations](#migrations) + * [Views](#views) + * [Internationalization](#internationalization) + * [Assets](#assets) + * [Mailers](#mailers) + * [Active Support Core Extensions](#active-support-core-extensions) + * [Time](#time) + * [Bundler](#bundler) + * [Managing processes](#managing-processes) ## Configuration -* - Put custom initialization code in `config/initializers`. The code in - initializers executes on application startup. -[[link](#config-initializers)] + * + Put custom initialization code in `config/initializers`. The code in + initializers executes on application startup. + [[link](#config-initializers)] -* - Keep initialization code for each gem in a separate file with the same name - as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc. -[[link](#gem-initializers)] + * + Keep initialization code for each gem in a separate file with the same name + as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc. + [[link](#gem-initializers)] -* - Adjust accordingly the settings for development, test and production - environment (in the corresponding files under `config/environments/`) -[[link](#dev-test-prod-configs)] + * + Adjust accordingly the settings for development, test and production + environment (in the corresponding files under `config/environments/`) + [[link](#dev-test-prod-configs)] - * Mark additional assets for precompilation (if any): + * Mark additional assets for precompilation (if any): - ```ruby - # 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.css rails_admin/rails_admin.js ) - ``` + ```ruby + # 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.css rails_admin/rails_admin.js ) + ``` -* - Keep configuration that's applicable to all environments in the - `config/application.rb` file. -[[link](#app-config)] + * + Keep configuration that's applicable to all environments in the + `config/application.rb` file. + [[link](#app-config)] -* - Create an additional `staging` environment that closely resembles the - `production` one. -[[link](#staging-like-prod)] + * + Create an additional `staging` environment that closely resembles the + `production` one. + [[link](#staging-like-prod)] -* - Keep any additional configuration in YAML files under the `config/` directory. -[[link](#yaml-config)] + * + Keep any additional configuration in YAML files under the `config/` directory. + [[link](#yaml-config)] - Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method: + Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method: - ```ruby - Rails::Application.config_for(:yaml_file) - ``` + ```ruby + Rails::Application.config_for(:yaml_file) + ``` ## Routing -* - When you need to add more actions to a RESTful resource (do you really need - them at all?) use `member` and `collection` routes. -[[link](#member-collection-routes)] + * + When you need to add more actions to a RESTful resource (do you really need + them at all?) use `member` and `collection` routes. + [[link](#member-collection-routes)] - ```ruby - # bad - get 'subscriptions/:id/unsubscribe' - resources :subscriptions + ```ruby + # bad + get 'subscriptions/:id/unsubscribe' + resources :subscriptions - # good - resources :subscriptions do - get 'unsubscribe', on: :member - end + # good + resources :subscriptions do + get 'unsubscribe', on: :member + end - # bad - get 'photos/search' - resources :photos + # bad + get 'photos/search' + resources :photos - # good - resources :photos do - get 'search', on: :collection - end - ``` - -* - If you need to define multiple `member/collection` routes use the - alternative block syntax. -[[link](#many-member-collection-routes)] - - ```ruby - resources :subscriptions do - member do - get 'unsubscribe' - # more routes + # good + resources :photos do + get 'search', on: :collection end - end + ``` - resources :photos do - collection do - get 'search' - # more routes + * + If you need to define multiple `member/collection` routes use the + alternative block syntax. + [[link](#many-member-collection-routes)] + + ```ruby + resources :subscriptions do + member do + get 'unsubscribe' + # more routes + end end - end - ``` -* - Use nested routes to express better the relationship between ActiveRecord - models. -[[link](#nested-routes)] + resources :photos do + collection do + get 'search' + # more routes + end + end + ``` - ```ruby - class Post < ActiveRecord::Base - has_many :comments - end + * + Use nested routes to express better the relationship between ActiveRecord + models. + [[link](#nested-routes)] - class Comment < ActiveRecord::Base - belongs_to :post - end + ```ruby + class Post < ActiveRecord::Base + has_many :comments + end - # routes.rb - resources :posts do - resources :comments - end - ``` + class Comment < ActiveRecord::Base + belongs_to :post + end + + # routes.rb + resources :posts do + resources :comments + end + ``` -* - If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`. + * + If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`. - ```ruby - resources :posts, shallow: true do - resources :comments do - resources :versions + ```ruby + resources :posts, shallow: true do + resources :comments do + resources :versions + end end - end - ``` + ``` -* - Use namespaced routes to group related actions. -[[link](#namespaced-routes)] + * + Use namespaced routes to group related actions. + [[link](#namespaced-routes)] - ```ruby - namespace :admin do - # Directs /admin/products/* to Admin::ProductsController - # (app/controllers/admin/products_controller.rb) - resources :products - end - ``` + ```ruby + namespace :admin do + # Directs /admin/products/* to Admin::ProductsController + # (app/controllers/admin/products_controller.rb) + resources :products + end + ``` -* - Never use the legacy wild controller route. This route will make all actions - in every controller accessible via GET requests. -[[link](#no-wild-routes)] + * + Never use the legacy wild controller route. This route will make all actions + in every controller accessible via GET requests. + [[link](#no-wild-routes)] - ```ruby - # very bad - match ':controller(/:action(/:id(.:format)))' - ``` + ```ruby + # very bad + match ':controller(/:action(/:id(.:format)))' + ``` -* - Don't use `match` 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. -[[link](#no-match-routes)] + * + Don't use `match` 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. + [[link](#no-match-routes)] ## Controllers -* - 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). -[[link](#skinny-controllers)] + * + 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). + [[link](#skinny-controllers)] -* - Each controller action should (ideally) invoke only one method other than an - initial find or new. -[[link](#one-method)] + * + Each controller action should (ideally) invoke only one method other than an + initial find or new. + [[link](#one-method)] -* - Share no more than two instance variables between a controller and a view. -[[link](#shared-instance-variables)] + * + Share no more than two instance variables between a controller and a view. + [[link](#shared-instance-variables)] -* - 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. -[[link](#lexically-scoped-action-filter)] + * + 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. + [[link](#lexically-scoped-action-filter)] ```ruby # bad @@ -250,9 +250,9 @@ end ### Rendering -* - Prefer using a template over inline rendering. -[[link](#inline-rendering)] + * + Prefer using a template over inline rendering. + [[link](#inline-rendering)] ```ruby # very bad @@ -278,9 +278,9 @@ class ProductsController < ApplicationController end ``` -* - Prefer `render plain:` over `render text:`. -[[link](#plain-text-rendering)] + * + Prefer `render plain:` over `render text:`. + [[link](#plain-text-rendering)] ```ruby # bad - sets MIME type to `text/html` @@ -299,9 +299,9 @@ render plain: 'Ruby!' ... ``` -* - Prefer [corresponding symbols](https://gist.github.com/mlanett/a31c340b132ddefa9cca) to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes. -[[link](#http-status-code-symbols)] + * + Prefer [corresponding symbols](https://gist.github.com/mlanett/a31c340b132ddefa9cca) to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes. + [[link](#http-status-code-symbols)] ```ruby # bad @@ -317,1062 +317,1062 @@ render status: :forbidden ## Models -* - Introduce non-ActiveRecord model classes freely. -[[link](#model-classes)] + * + Introduce non-ActiveRecord model classes freely. + [[link](#model-classes)] -* - Name the models with meaningful (but short) names without abbreviations. -[[link](#meaningful-model-names)] + * + Name the models with meaningful (but short) names without abbreviations. + [[link](#meaningful-model-names)] -* - If you need model objects that support ActiveRecord behavior (like validation) - without the ActiveRecord database functionality use the - [ActiveAttr](https://github.com/cgriego/active_attr) gem. -[[link](#activeattr-gem)] + * + If you need model objects that support ActiveRecord behavior (like validation) + without the ActiveRecord database functionality use the + [ActiveAttr](https://github.com/cgriego/active_attr) gem. + [[link](#activeattr-gem)] - ```ruby - class Message - include ActiveAttr::Model + ```ruby + class Message + include ActiveAttr::Model - attribute :name - attribute :email - attribute :content - attribute :priority + attribute :name + attribute :email + attribute :content + attribute :priority - attr_accessible :name, :email, :content + attr_accessible :name, :email, :content - validates :name, presence: true - validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } - validates :content, length: { maximum: 500 } - end - ``` + validates :name, presence: true + validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } + validates :content, length: { maximum: 500 } + end + ``` - For a more complete example refer to the - [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr). + For a more complete example refer to the + [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr). -* - 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. -[[link](#model-business-logic)] + * + 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. + [[link](#model-business-logic)] ### ActiveRecord -* - Avoid altering ActiveRecord defaults (table names, primary key, etc) unless - you have a very good reason (like a database that's not under your control). -[[link](#keep-ar-defaults)] + * + Avoid altering ActiveRecord defaults (table names, primary key, etc) unless + you have a very good reason (like a database that's not under your control). + [[link](#keep-ar-defaults)] - ```ruby - # bad - don't do this if you can modify the schema - class Transaction < ActiveRecord::Base - self.table_name = 'order' - ... - end - ``` - -* - Group macro-style methods (`has_many`, `validates`, etc) in the beginning of - the class definition. -[[link](#macro-style-methods)] + ```ruby + # bad - don't do this if you can modify the schema + class Transaction < ActiveRecord::Base + self.table_name = 'order' + ... + end + ``` - ```ruby - class User < ActiveRecord::Base - # keep the default scope first (if any) - default_scope { where(active: true) } + * + Group macro-style methods (`has_many`, `validates`, etc) in the beginning of + the class definition. + [[link](#macro-style-methods)] - # constants come up next - COLORS = %w(red green blue) + ```ruby + class User < ActiveRecord::Base + # keep the default scope first (if any) + default_scope { where(active: true) } - # afterwards we put attr related macros - attr_accessor :formatted_date_of_birth + # constants come up next + COLORS = %w(red green blue) - attr_accessible :login, :first_name, :last_name, :email, :password + # afterwards we put attr related macros + attr_accessor :formatted_date_of_birth - # Rails4+ enums after attr macros, prefer the hash syntax - enum gender: { female: 0, male: 1 } + attr_accessible :login, :first_name, :last_name, :email, :password - # followed by association macros - belongs_to :country + # Rails4+ enums after attr macros, prefer the hash syntax + enum gender: { female: 0, male: 1 } - has_many :authentications, dependent: :destroy + # followed by association macros + belongs_to :country - # and validation macros - validates :email, presence: true - validates :username, presence: true - validates :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 } + has_many :authentications, dependent: :destroy - # next we have callbacks - before_save :cook - before_save :update_username_lower + # and validation macros + validates :email, presence: true + validates :username, presence: true + validates :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 } - # other macros (like devise's) should be placed after the callbacks + # next we have callbacks + before_save :cook + before_save :update_username_lower - ... - end - ``` + # other macros (like devise's) should be placed after the callbacks -* - Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many - :through` allows additional attributes and validations on the join model. -[[link](#has-many-through)] + ... + end + ``` - ```ruby - # not so good - using has_and_belongs_to_many - class User < ActiveRecord::Base - has_and_belongs_to_many :groups - end + * + Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many + :through` allows additional attributes and validations on the join model. + [[link](#has-many-through)] - class Group < ActiveRecord::Base - has_and_belongs_to_many :users - end + ```ruby + # not so good - using has_and_belongs_to_many + class User < ActiveRecord::Base + has_and_belongs_to_many :groups + end - # preferred way - using has_many :through - class User < ActiveRecord::Base - has_many :memberships - has_many :groups, through: :memberships - end + class Group < ActiveRecord::Base + has_and_belongs_to_many :users + end - class Membership < ActiveRecord::Base - belongs_to :user - belongs_to :group - end + # preferred way - using has_many :through + class User < ActiveRecord::Base + has_many :memberships + has_many :groups, through: :memberships + end - class Group < ActiveRecord::Base - has_many :memberships - has_many :users, through: :memberships - end - ``` + class Membership < ActiveRecord::Base + belongs_to :user + belongs_to :group + end -* - Prefer `self[:attribute]` over `read_attribute(:attribute)`. -[[link](#read-attribute)] + class Group < ActiveRecord::Base + has_many :memberships + has_many :users, through: :memberships + end + ``` - ```ruby - # bad - def amount - read_attribute(:amount) * 100 - end + * + Prefer `self[:attribute]` over `read_attribute(:attribute)`. + [[link](#read-attribute)] - # good - def amount - self[:amount] * 100 - end - ``` + ```ruby + # bad + def amount + read_attribute(:amount) * 100 + end -* - Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. -[[link](#write-attribute)] + # good + def amount + self[:amount] * 100 + end + ``` - ```ruby - # bad - def amount - write_attribute(:amount, 100) - end + * + Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. + [[link](#write-attribute)] - # good - def amount - self[:amount] = 100 - end - ``` - -* - Always use the new ["sexy" - validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). -[[link](#sexy-validations)] - - ```ruby - # bad - validates_presence_of :email - validates_length_of :email, maximum: 100 - - # good - validates :email, presence: true, length: { maximum: 100 } - ``` - -* - To make validations easy to read, don't list multiple attributes per - validation -[[link](#single-attribute-validations)] - - ```ruby - # bad - validates :email, :password, presence: true - validates :email, length: { maximum: 100 } - - # good - validates :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. -[[link](#custom-validator-file)] - - ```ruby - # bad - class Person - validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } - end + ```ruby + # bad + def amount + write_attribute(:amount, 100) + end - # good - class EmailValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i + # good + def amount + self[:amount] = 100 end - end + ``` - class Person - validates :email, email: true - end - ``` + * + Always use the new ["sexy" + validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). + [[link](#sexy-validations)] -* - Keep custom validators under `app/validators`. -[[link](#app-validators)] + ```ruby + # bad + validates_presence_of :email + validates_length_of :email, maximum: 100 -* - Consider extracting custom validators to a shared gem if you're maintaining - several related apps or the validators are generic enough. -[[link](#custom-validators-gem)] + # good + validates :email, presence: true, length: { maximum: 100 } + ``` -* - Use named scopes freely. -[[link](#named-scopes)] + * + To make validations easy to read, don't list multiple attributes per + validation + [[link](#single-attribute-validations)] - ```ruby - class User < ActiveRecord::Base - scope :active, -> { where(active: true) } - scope :inactive, -> { where(active: false) } + ```ruby + # bad + validates :email, :password, presence: true + validates :email, length: { maximum: 100 } - 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 an `ActiveRecord::Relation` - object. Arguably you can define even simpler scopes like this. -[[link](#named-scope-class)] - - ```ruby - class User < ActiveRecord::Base - def self.with_orders - joins(:orders).select('distinct(users.id)') + # good + validates :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. + [[link](#custom-validator-file)] + + ```ruby + # bad + class Person + validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } end - end - ``` - -* - Order callback declarations in the order, in which they will be executed. For - referenece, see [Available Callbacks](http://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks) -[[link](#callbacks-order)] - - ```Ruby - #bad - class Person - after_commit/after_rollback :after_commit_callback - after_save :after_save_callback - around_save :around_save_callback - after_update :after_update_callback - before_update :before_update_callback - after_validation :after_validation_callback - before_validation :before_validation_callback - before_save :before_save_callback - before_create :before_create_callback - after_create :after_create_callback - around_create :around_create_callback - around_update :around_update_callback - end - #good - class Person - before_validation :before_validation_callback - after_validation :after_validation_callback - before_save :before_save_callback - around_save :around_save_callback - before_create :before_create_callback - around_create :around_create_callback - after_create :after_create_callback - before_update :before_update_callback - around_update :around_update_callback - after_update :after_update_callback - after_save :after_save_callback - after_commit/after_rollback :after_commit_callback - end - ``` - -* - Beware of the behavior of the - [following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations) - methods. They do not run the model validations and - could easily corrupt the model state. -[[link](#beware-skip-model-validations)] - - ```ruby - # bad - Article.first.decrement!(:view_count) - DiscussionBoard.decrement_counter(:post_count, 5) - Article.first.increment!(:view_count) - DiscussionBoard.increment_counter(:post_count, 5) - person.toggle :active - product.touch - Billing.update_all("category = 'authorized', author = 'David'") - user.update_attribute(:website, 'example.com') - user.update_columns(last_request_at: Time.current) - Post.update_counters 5, comment_count: -1, action_count: 1 - - # good - user.update_attributes(website: 'example.com') - ``` - -* - Use user-friendly URLs. Show some descriptive attribute of the model in the URL - rather than its `id`. There is more than one way to achieve this: -[[link](#user-friendly-urls)] - - * Override the `to_param` method of the model. This method is used by Rails - for constructing a URL to the object. The default implementation returns - the `id` of the record as a String. It could be overridden to include another - human-readable attribute. - - ```ruby - class Person - def to_param - "#{id} #{name}".parameterize - end + # good + class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i end - ``` + end - In order to convert this to a URL-friendly value, `parameterize` should be - called on the string. The `id` of the object needs to be at the beginning so - that it can be found by the `find` method of ActiveRecord. + class Person + validates :email, email: true + end + ``` + + * + Keep custom validators under `app/validators`. + [[link](#app-validators)] + + * + Consider extracting custom validators to a shared gem if you're maintaining + several related apps or the validators are generic enough. + [[link](#custom-validators-gem)] + + * + Use named scopes freely. + [[link](#named-scopes)] + + ```ruby + class User < ActiveRecord::Base + scope :active, -> { where(active: true) } + scope :inactive, -> { where(active: false) } - * Use the `friendly_id` gem. It allows creation of human-readable URLs by - using some descriptive attribute of the model instead of its `id`. + scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } + end + ``` - ```ruby - class Person - extend FriendlyId - friendly_id :name, use: :slugged + * + 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 an `ActiveRecord::Relation` + object. Arguably you can define even simpler scopes like this. + [[link](#named-scope-class)] + + ```ruby + class User < ActiveRecord::Base + def self.with_orders + joins(:orders).select('distinct(users.id)') end - ``` + end + ``` - Check the [gem documentation](https://github.com/norman/friendly_id) for more - information about its usage. + * + Order callback declarations in the order, in which they will be executed. For + referenece, see [Available Callbacks](http://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks) + [[link](#callbacks-order)] + + ```Ruby + #bad + class Person + after_commit/after_rollback :after_commit_callback + after_save :after_save_callback + around_save :around_save_callback + after_update :after_update_callback + before_update :before_update_callback + after_validation :after_validation_callback + before_validation :before_validation_callback + before_save :before_save_callback + before_create :before_create_callback + after_create :after_create_callback + around_create :around_create_callback + around_update :around_update_callback + end -* - Use `find_each` to iterate over a collection of AR objects. Looping through a - collection of records from the database (using the `all` 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. -[[link](#find-each)] + #good + class Person + before_validation :before_validation_callback + after_validation :after_validation_callback + before_save :before_save_callback + around_save :around_save_callback + before_create :before_create_callback + around_create :around_create_callback + after_create :after_create_callback + before_update :before_update_callback + around_update :around_update_callback + after_update :after_update_callback + after_save :after_save_callback + after_commit/after_rollback :after_commit_callback + end + ``` + * + Beware of the behavior of the + [following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations) + methods. They do not run the model validations and + could easily corrupt the model state. + [[link](#beware-skip-model-validations)] - ```ruby - # bad - Person.all.each do |person| - person.do_awesome_stuff - end + ```ruby + # bad + Article.first.decrement!(:view_count) + DiscussionBoard.decrement_counter(:post_count, 5) + Article.first.increment!(:view_count) + DiscussionBoard.increment_counter(:post_count, 5) + person.toggle :active + product.touch + Billing.update_all("category = 'authorized', author = 'David'") + user.update_attribute(:website, 'example.com') + user.update_columns(last_request_at: Time.current) + Post.update_counters 5, comment_count: -1, action_count: 1 + + # good + user.update_attributes(website: 'example.com') + ``` - Person.where('age > 21').each do |person| - person.party_all_night! - end + * + Use user-friendly URLs. Show some descriptive attribute of the model in the URL + rather than its `id`. There is more than one way to achieve this: + [[link](#user-friendly-urls)] + + * Override the `to_param` method of the model. This method is used by Rails + for constructing a URL to the object. The default implementation returns + the `id` of the record as a String. It could be overridden to include another + human-readable attribute. + + ```ruby + class Person + def to_param + "#{id} #{name}".parameterize + end + end + ``` - # good - Person.find_each do |person| - person.do_awesome_stuff - end + In order to convert this to a URL-friendly value, `parameterize` should be + called on the string. The `id` of the object needs to be at the beginning so + that it can be found by the `find` method of ActiveRecord. - Person.where('age > 21').find_each do |person| - person.party_all_night! - end - ``` + * Use the `friendly_id` gem. It allows creation of human-readable URLs by + using some descriptive attribute of the model instead of its `id`. -* - Since [Rails creates callbacks for dependent - associations](https://github.com/rails/rails/issues/3458), always call - `before_destroy` callbacks that perform validation with `prepend: true`. -[[link](#before_destroy)] + ```ruby + class Person + extend FriendlyId + friendly_id :name, use: :slugged + end + ``` - ```ruby - # bad (roles will be deleted automatically even if super_admin? is true) - has_many :roles, dependent: :destroy + Check the [gem documentation](https://github.com/norman/friendly_id) for more + information about its usage. - before_destroy :ensure_deletable + * + Use `find_each` to iterate over a collection of AR objects. Looping through a + collection of records from the database (using the `all` 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. + [[link](#find-each)] - def ensure_deletable - raise "Cannot delete super admin." if super_admin? - end - # good - has_many :roles, dependent: :destroy + ```ruby + # bad + Person.all.each do |person| + person.do_awesome_stuff + end - before_destroy :ensure_deletable, prepend: true + Person.where('age > 21').each do |person| + person.party_all_night! + end - def ensure_deletable - raise "Cannot delete super admin." if super_admin? - end - ``` + # good + Person.find_each do |person| + person.do_awesome_stuff + end -* - Define the `dependent` option to the `has_many` and `has_one` associations. -[[link](#has_many-has_one-dependent-option)] + Person.where('age > 21').find_each do |person| + person.party_all_night! + end + ``` - ```ruby - # bad - class Post < ActiveRecord::Base - has_many :comments - end + * + Since [Rails creates callbacks for dependent + associations](https://github.com/rails/rails/issues/3458), always call + `before_destroy` callbacks that perform validation with `prepend: true`. + [[link](#before_destroy)] - # good - class Post < ActiveRecord::Base - has_many :comments, dependent: :destroy - end - ``` - -* - When persisting AR objects always use the exception raising bang! method or handle the method return value. - This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. -[[link](#save-bang)] - - ```ruby - # bad - user.create(name: 'Bruce') - - # bad - user.save - - # good - user.create!(name: 'Bruce') - # or - bruce = user.create(name: 'Bruce') - if bruce.persisted? - ... - else - ... - end + ```ruby + # bad (roles will be deleted automatically even if super_admin? is true) + has_many :roles, dependent: :destroy - # good - user.save! - # or - if user.save - ... - else - ... - end - ``` + before_destroy :ensure_deletable -### ActiveRecord Queries + def ensure_deletable + raise "Cannot delete super admin." if super_admin? + end -* - Avoid string interpolation in - queries, as it will make your code susceptible to SQL injection - attacks. -[[link](#avoid-interpolation)] - - ```ruby - # bad - param will be interpolated unescaped - Client.where("orders_count = #{params[:orders]}") - - # good - param will be properly escaped - Client.where('orders_count = ?', params[:orders]) - ``` - -* - Consider using named placeholders instead of positional placeholders - when you have more than 1 placeholder in your query. -[[link](#named-placeholder)] - - ```ruby - # okish - Client.where( - 'created_at >= ? AND created_at <= ?', - params[:start_date], params[:end_date] - ) - - # good - Client.where( - 'created_at >= :start_date AND created_at <= :end_date', - start_date: params[:start_date], end_date: params[:end_date] - ) - ``` - -* - Favor the use of `find` over `where.take!`, `find_by!`, and `find_by_id!` - when you need to retrieve a single record by primary key id and raise - `ActiveRecord::RecordNotFound` when the record is not found. -[[link](#find)] - - ```ruby - # bad - User.where(id: id).take! - - # bad - User.find_by_id!(id) - - # bad - User.find_by!(id: id) - - # good - User.find(id) - ``` - -* - Favor the use of `find_by` over `where.take` and `find_by_attribute` - when you need to retrieve a single record by one or more attributes and return - `nil` when the record is not found. -[[link](#find_by)] - - ```ruby - # bad - User.where(id: id).take - User.where(first_name: 'Bruce', last_name: 'Wayne').take - - # bad - User.find_by_id(id) - # bad, deprecated in ActiveRecord 4.0, removed in 4.1+ - User.find_by_first_name_and_last_name('Bruce', 'Wayne') - - # good - User.find_by(id: id) - User.find_by(first_name: 'Bruce', last_name: 'Wayne') - ``` - -* - Favor the use of `where.not` over SQL. -[[link](#where-not)] - - ```ruby - # bad - User.where("id != ?", id) - - # good - User.where.not(id: id) - ``` - -* - Don't use the `id` 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. -[[link](#order-by-id)] - - ```ruby - # bad - scope :chronological, -> { order(id: :asc) } - - # good - scope :chronological, -> { order(created_at: :asc) } - ``` - -* - Favor the use of `ids` over `pluck(:id)`. -[[link](#ids)] - - ```Ruby - # bad - User.pluck(:id) - - # good - User.ids - ``` - -* - When specifying an explicit query in a method such as `find_by_sql`, use - heredocs with `squish`. 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). -[[link](#squished-heredocs)] - - ```ruby - 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`](http://apidock.com/rails/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 acounts\n ON\n accounts.user_id = users.id - ``` - -* - When querying ActiveRecord collections, prefer `size` (selects between count/length behavior based on whether collection is already loaded) or `length` (always loads the whole collection and counts the array elements) over `count` (always does a database query for the count). -[[link](#size-over-count-or-length)] - - ```ruby - # bad - User.count - - # good - User.all.size - - # good - if you really need to load all users into memory - User.all.length - ``` + # good + has_many :roles, dependent: :destroy -## Migrations + before_destroy :ensure_deletable, prepend: true -* - Keep the `schema.rb` (or `structure.sql`) under version control. -[[link](#schema-version)] + def ensure_deletable + raise "Cannot delete super admin." if super_admin? + end + ``` -* - Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty - database. -[[link](#db-schema-load)] + * + Define the `dependent` option to the `has_many` and `has_one` associations. + [[link](#has_many-has_one-dependent-option)] -* - Enforce default values in the migrations themselves instead of in the - application layer. -[[link](#default-migration-values)] + ```ruby + # bad + class Post < ActiveRecord::Base + has_many :comments + end - ```ruby - # bad - application enforced default value - class Product < ActiveRecord::Base - def amount - self[:amount] || 0 + # good + class Post < ActiveRecord::Base + has_many :comments, dependent: :destroy end - end + ``` - # good - database enforced - class AddDefaultAmountToProducts < ActiveRecord::Migration - def change - change_column_default :products, :amount, 0 + * + When persisting AR objects always use the exception raising bang! method or handle the method return value. + This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. + [[link](#save-bang)] + + ```ruby + # bad + user.create(name: 'Bruce') + + # bad + user.save + + # good + user.create!(name: 'Bruce') + # or + bruce = user.create(name: 'Bruce') + if bruce.persisted? + ... + else + ... end - end - ``` - - 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. - -* - Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord - supports foreign key constraints natively. - [[link](#foreign-key-constraints)] - -* - When writing constructive migrations (adding tables or columns), - use the `change` method instead of `up` and `down` methods. - [[link](#change-vs-up-down)] - - ```ruby - # the old way - class AddNameToPeople < ActiveRecord::Migration - def up - add_column :people, :name, :string + + # good + user.save! + # or + if user.save + ... + else + ... end + ``` - def down - remove_column :people, :name +### ActiveRecord Queries + + * + Avoid string interpolation in + queries, as it will make your code susceptible to SQL injection + attacks. + [[link](#avoid-interpolation)] + + ```ruby + # bad - param will be interpolated unescaped + Client.where("orders_count = #{params[:orders]}") + + # good - param will be properly escaped + Client.where('orders_count = ?', params[:orders]) + ``` + + * + Consider using named placeholders instead of positional placeholders + when you have more than 1 placeholder in your query. + [[link](#named-placeholder)] + + ```ruby + # okish + Client.where( + 'created_at >= ? AND created_at <= ?', + params[:start_date], params[:end_date] + ) + + # good + Client.where( + 'created_at >= :start_date AND created_at <= :end_date', + start_date: params[:start_date], end_date: params[:end_date] + ) + ``` + + * + Favor the use of `find` over `where.take!`, `find_by!`, and `find_by_id!` + when you need to retrieve a single record by primary key id and raise + `ActiveRecord::RecordNotFound` when the record is not found. + [[link](#find)] + + ```ruby + # bad + User.where(id: id).take! + + # bad + User.find_by_id!(id) + + # bad + User.find_by!(id: id) + + # good + User.find(id) + ``` + + * + Favor the use of `find_by` over `where.take` and `find_by_attribute` + when you need to retrieve a single record by one or more attributes and return + `nil` when the record is not found. + [[link](#find_by)] + + ```ruby + # bad + User.where(id: id).take + User.where(first_name: 'Bruce', last_name: 'Wayne').take + + # bad + User.find_by_id(id) + # bad, deprecated in ActiveRecord 4.0, removed in 4.1+ + User.find_by_first_name_and_last_name('Bruce', 'Wayne') + + # good + User.find_by(id: id) + User.find_by(first_name: 'Bruce', last_name: 'Wayne') + ``` + + * + Favor the use of `where.not` over SQL. + [[link](#where-not)] + + ```ruby + # bad + User.where("id != ?", id) + + # good + User.where.not(id: id) + ``` + + * + Don't use the `id` 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. + [[link](#order-by-id)] + + ```ruby + # bad + scope :chronological, -> { order(id: :asc) } + + # good + scope :chronological, -> { order(created_at: :asc) } + ``` + + * + Favor the use of `ids` over `pluck(:id)`. + [[link](#ids)] + + ```Ruby + # bad + User.pluck(:id) + + # good + User.ids + ``` + + * + When specifying an explicit query in a method such as `find_by_sql`, use + heredocs with `squish`. 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). + [[link](#squished-heredocs)] + + ```ruby + 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`](http://apidock.com/rails/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 acounts\n ON\n accounts.user_id = users.id + ``` + + * + When querying ActiveRecord collections, prefer `size` (selects between count/length behavior based on whether collection is already loaded) or `length` (always loads the whole collection and counts the array elements) over `count` (always does a database query for the count). + [[link](#size-over-count-or-length)] + + ```ruby + # bad + User.count + + # good + User.all.size + + # good - if you really need to load all users into memory + User.all.length + ``` + +## Migrations + + * + Keep the `schema.rb` (or `structure.sql`) under version control. + [[link](#schema-version)] + + * + Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty + database. + [[link](#db-schema-load)] + + * + Enforce default values in the migrations themselves instead of in the + application layer. + [[link](#default-migration-values)] + + ```ruby + # bad - application enforced default value + class Product < ActiveRecord::Base + def amount + self[:amount] || 0 + end end - end - # the new preferred way - class AddNameToPeople < ActiveRecord::Migration - def change - add_column :people, :name, :string + # good - database enforced + class AddDefaultAmountToProducts < ActiveRecord::Migration + def change + change_column_default :products, :amount, 0 + end end - end - ``` - -* - 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 -[[link](#define-model-class-migrations)] - - ```ruby - # db/migrate/.rb - # frozen_string_literal: true - - # bad - class ModifyDefaultStatusForProducts < ActiveRecord::Migration - def change - old_status = 'pending_manual_approval' - new_status = 'pending_approval' - - reversible do |dir| - dir.up do - Product.where(status: old_status).update_all(status: new_status) - change_column :products, :status, :string, default: new_status - end + ``` - dir.down do - Product.where(status: new_status).update_all(status: old_status) - change_column :products, :status, :string, default: old_status - end + 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. + + * + Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord + supports foreign key constraints natively. + [[link](#foreign-key-constraints)] + + * + When writing constructive migrations (adding tables or columns), + use the `change` method instead of `up` and `down` methods. + [[link](#change-vs-up-down)] + + ```ruby + # the old way + class AddNameToPeople < ActiveRecord::Migration + def up + add_column :people, :name, :string + end + + def down + remove_column :people, :name end end - end - # 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. - class MigrationProduct < ActiveRecord::Base - self.table_name = :products - end + # the new preferred way + class AddNameToPeople < ActiveRecord::Migration + def change + add_column :people, :name, :string + end + end + ``` - class ModifyDefaultStatusForProducts < ActiveRecord::Migration - def change - old_status = 'pending_manual_approval' - new_status = 'pending_approval' + * + 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 + [[link](#define-model-class-migrations)] - reversible do |dir| - dir.up do - MigrationProduct.where(status: old_status).update_all(status: new_status) - change_column :products, :status, :string, default: new_status + ```ruby + # db/migrate/.rb + # frozen_string_literal: true + + # bad + class ModifyDefaultStatusForProducts < ActiveRecord::Migration + def change + old_status = 'pending_manual_approval' + new_status = 'pending_approval' + + reversible do |dir| + dir.up do + Product.where(status: old_status).update_all(status: new_status) + change_column :products, :status, :string, default: new_status + end + + dir.down do + Product.where(status: new_status).update_all(status: old_status) + change_column :products, :status, :string, default: old_status + end end + end + end - dir.down do - MigrationProduct.where(status: new_status).update_all(status: old_status) - change_column :products, :status, :string, default: old_status + # 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. + class MigrationProduct < ActiveRecord::Base + self.table_name = :products + end + + class ModifyDefaultStatusForProducts < ActiveRecord::Migration + def change + old_status = 'pending_manual_approval' + new_status = 'pending_approval' + + reversible do |dir| + dir.up do + MigrationProduct.where(status: old_status).update_all(status: new_status) + change_column :products, :status, :string, default: new_status + end + + dir.down do + MigrationProduct.where(status: new_status).update_all(status: old_status) + change_column :products, :status, :string, default: old_status + end end end end - end - ``` - -* - Name your foreign keys explicitly instead of relying on Rails auto-generated - FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) -[[link](#meaningful-foreign-key-naming)] - - ```ruby - # bad - class AddFkArticlesToAuthors < ActiveRecord::Migration - def change - add_foreign_key :articles, :authors - end - end + ``` + + * + Name your foreign keys explicitly instead of relying on Rails auto-generated + FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) + [[link](#meaningful-foreign-key-naming)] - # good - class AddFkArticlesToAuthors < ActiveRecord::Migration - def change - add_foreign_key :articles, :authors, name: :articles_author_id_fk + ```ruby + # bad + class AddFkArticlesToAuthors < ActiveRecord::Migration + def change + add_foreign_key :articles, :authors + end end - end - ``` - -* - Don't use non-reversible migration commands in the `change` method. - Reversible migration commands are listed below. - [ActiveRecord::Migration::CommandRecorder](http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html) -[[link](#reversible-migration)] - - ```ruby - # bad - class DropUsers < ActiveRecord::Migration - def change - drop_table :users + + # good + class AddFkArticlesToAuthors < ActiveRecord::Migration + def change + add_foreign_key :articles, :authors, name: :articles_author_id_fk + end end - end + ``` + + * + Don't use non-reversible migration commands in the `change` method. + Reversible migration commands are listed below. + [ActiveRecord::Migration::CommandRecorder](http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html) + [[link](#reversible-migration)] - # good - class DropUsers < ActiveRecord::Migration - def up - drop_table :users + ```ruby + # bad + class DropUsers < ActiveRecord::Migration + def change + drop_table :users + end end - def down - create_table :users do |t| - t.string :name + # good + class DropUsers < ActiveRecord::Migration + def up + drop_table :users + end + + def down + create_table :users do |t| + t.string :name + end end end - end - # good - # In this case, block will be used by create_table in rollback - # http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table - class DropUsers < ActiveRecord::Migration - def change - drop_table :users do |t| - t.string :name + # good + # In this case, block will be used by create_table in rollback + # http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table + class DropUsers < ActiveRecord::Migration + def change + drop_table :users do |t| + t.string :name + end end end - end - ``` + ``` ## Views -* - Never call the model layer directly from a view. -[[link](#no-direct-model-view)] + * + Never call the model layer directly from a view. + [[link](#no-direct-model-view)] -* - Never make complex formatting in the views, export the formatting to a method - in the view helper or the model. -[[link](#no-complex-view-formatting)] + * + Never make complex formatting in the views, export the formatting to a method + in the view helper or the model. + [[link](#no-complex-view-formatting)] -* - Mitigate code duplication by using partial templates and layouts. -[[link](#partials)] + * + Mitigate code duplication by using partial templates and layouts. + [[link](#partials)] ## Internationalization -* - 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 the - `config/locales` directory. -[[link](#locale-texts)] - -* - When the labels of an ActiveRecord model need to be translated, use the - `activerecord` scope: -[[link](#translated-labels)] - - ``` - en: - activerecord: - models: - user: Member - attributes: - user: - name: 'Full name' - ``` - - Then `User.model_name.human` will return "Member" and - `User.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 ActiveRecord - attributes. Place the locale files for the models in a folder `locales/models` and the - texts used in the views in folder `locales/views`. -[[link](#organize-locale-files)] - - * When organization of the locale files is done with additional directories, - these directories must be described in the `application.rb` file in order - to be loaded. - - ```ruby - # config/application.rb - config.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 the `locales` directory. -[[link](#shared-localization)] - -* - Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` - and `I18n.l` instead of `I18n.localize`. -[[link](#short-i18n)] - -* - Use "lazy" lookup for the texts used in views. Let's say we have the following - structure: -[[link](#lazy-lookup)] - - ``` - en: - users: - show: - title: 'User details page' - ``` - - The value for `users.show.title` can be looked up in the template - `app/views/users/show.html.haml` like this: - - ```ruby - = t '.title' - ``` - -* - Use the dot-separated keys in the controllers and models instead of specifying - the `:scope` option. The dot-separated call is easier to read and trace the - hierarchy. -[[link](#dot-separated-keys)] - - ```ruby - # bad - I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] - - # good - I18n.t 'activerecord.errors.messages.record_invalid' - ``` - -* - More detailed information about the Rails I18n can be found in the [Rails - Guides](http://guides.rubyonrails.org/i18n.html) -[[link](#i18n-guides)] + * + 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 the + `config/locales` directory. + [[link](#locale-texts)] + + * + When the labels of an ActiveRecord model need to be translated, use the + `activerecord` scope: + [[link](#translated-labels)] + + ``` + en: + activerecord: + models: + user: Member + attributes: + user: + name: 'Full name' + ``` + + Then `User.model_name.human` will return "Member" and + `User.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 ActiveRecord + attributes. Place the locale files for the models in a folder `locales/models` and the + texts used in the views in folder `locales/views`. + [[link](#organize-locale-files)] + + * When organization of the locale files is done with additional directories, + these directories must be described in the `application.rb` file in order + to be loaded. + + ```ruby + # config/application.rb + config.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 the `locales` directory. + [[link](#shared-localization)] + + * + Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` + and `I18n.l` instead of `I18n.localize`. + [[link](#short-i18n)] + + * + Use "lazy" lookup for the texts used in views. Let's say we have the following + structure: + [[link](#lazy-lookup)] + + ``` + en: + users: + show: + title: 'User details page' + ``` + + The value for `users.show.title` can be looked up in the template + `app/views/users/show.html.haml` like this: + + ```ruby + = t '.title' + ``` + + * + Use the dot-separated keys in the controllers and models instead of specifying + the `:scope` option. The dot-separated call is easier to read and trace the + hierarchy. + [[link](#dot-separated-keys)] + + ```ruby + # bad + I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] + + # good + I18n.t 'activerecord.errors.messages.record_invalid' + ``` + + * + More detailed information about the Rails I18n can be found in the [Rails + Guides](http://guides.rubyonrails.org/i18n.html) + [[link](#i18n-guides)] ## Assets Use the [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within your application. -* - Reserve `app/assets` for custom stylesheets, javascripts, or images. -[[link](#reserve-app-assets)] - -* - Use `lib/assets` for your own libraries that don’t really fit into the - scope of the application. -[[link](#lib-assets)] - -* - Third party code such as [jQuery](http://jquery.com/) or - [bootstrap](http://twitter.github.com/bootstrap/) should be placed in - `vendor/assets`. -[[link](#vendor-assets)] - -* - When possible, use gemified versions of assets (e.g. - [jquery-rails](https://github.com/rails/jquery-rails), - [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails), - [bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass), - [zurb-foundation](https://github.com/zurb/foundation)). -[[link](#gem-assets)] + * + Reserve `app/assets` for custom stylesheets, javascripts, or images. + [[link](#reserve-app-assets)] + + * + Use `lib/assets` for your own libraries that don’t really fit into the + scope of the application. + [[link](#lib-assets)] + + * + Third party code such as [jQuery](http://jquery.com/) or + [bootstrap](http://twitter.github.com/bootstrap/) should be placed in + `vendor/assets`. + [[link](#vendor-assets)] + + * + When possible, use gemified versions of assets (e.g. + [jquery-rails](https://github.com/rails/jquery-rails), + [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails), + [bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass), + [zurb-foundation](https://github.com/zurb/foundation)). + [[link](#gem-assets)] ## Mailers -* - Name the mailers `SomethingMailer`. Without the Mailer suffix it isn't - immediately apparent what's a mailer and which views are related to the - mailer. -[[link](#mailer-name)] - -* - Provide both HTML and plain-text view templates. -[[link](#html-plain-email)] - -* - Enable errors raised on failed mail delivery in your development environment. - The errors are disabled by default. -[[link](#enable-delivery-errors)] - - ```ruby - # config/environments/development.rb - - config.action_mailer.raise_delivery_errors = true - ``` - -* - Use a local SMTP server like - [Mailcatcher](https://github.com/sj26/mailcatcher) in the development - environment. -[[link](#local-smtp)] - - ```ruby - # config/environments/development.rb - - config.action_mailer.smtp_settings = { - address: 'localhost', - port: 1025, - # more settings - } - ``` - -* - Provide default settings for the host name. -[[link](#default-hostname)] - - ```ruby - # config/environments/development.rb - config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } - - # config/environments/production.rb - config.action_mailer.default_url_options = { host: 'your_site.com' } - - # in your mailer class - default_url_options[:host] = 'your_site.com' - ``` - -* - If you need to use a link to your site in an email, always use the `_url`, not - `_path` methods. The `_url` methods include the host name and the `_path` - methods don't. -[[link](#url-not-path-in-email)] - - ```ruby - # bad - You can always find more info about this course - <%= link_to 'here', course_path(@course) %> - - # good - You can always find more info about this course - <%= link_to 'here', course_url(@course) %> - ``` - -* - Format the from and to addresses properly. Use the following format: -[[link](#email-addresses)] - - ```ruby - # in your mailer class - default from: 'Your Name ' - ``` - -* - Make sure that the e-mail delivery method for your test environment is set to - `test`: -[[link](#delivery-method-test)] - - ```ruby - # config/environments/test.rb - - config.action_mailer.delivery_method = :test - ``` - -* - The delivery method for development and production should be `smtp`: -[[link](#delivery-method-smtp)] - - ```ruby - # config/environments/development.rb, config/environments/production.rb - - config.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](https://github.com/fphilipe/premailer-rails) and - [roadie](https://github.com/Mange/roadie). -[[link](#inline-email-styles)] - -* - 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 - of [sidekiq](https://github.com/mperham/sidekiq) gem. -[[link](#background-email)] + * + Name the mailers `SomethingMailer`. Without the Mailer suffix it isn't + immediately apparent what's a mailer and which views are related to the + mailer. + [[link](#mailer-name)] + + * + Provide both HTML and plain-text view templates. + [[link](#html-plain-email)] + + * + Enable errors raised on failed mail delivery in your development environment. + The errors are disabled by default. + [[link](#enable-delivery-errors)] + + ```ruby + # config/environments/development.rb + + config.action_mailer.raise_delivery_errors = true + ``` + + * + Use a local SMTP server like + [Mailcatcher](https://github.com/sj26/mailcatcher) in the development + environment. + [[link](#local-smtp)] + + ```ruby + # config/environments/development.rb + + config.action_mailer.smtp_settings = { + address: 'localhost', + port: 1025, + # more settings + } + ``` + + * + Provide default settings for the host name. + [[link](#default-hostname)] + + ```ruby + # config/environments/development.rb + config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } + + # config/environments/production.rb + config.action_mailer.default_url_options = { host: 'your_site.com' } + + # in your mailer class + default_url_options[:host] = 'your_site.com' + ``` + + * + If you need to use a link to your site in an email, always use the `_url`, not + `_path` methods. The `_url` methods include the host name and the `_path` + methods don't. + [[link](#url-not-path-in-email)] + + ```ruby + # bad + You can always find more info about this course + <%= link_to 'here', course_path(@course) %> + + # good + You can always find more info about this course + <%= link_to 'here', course_url(@course) %> + ``` + + * + Format the from and to addresses properly. Use the following format: + [[link](#email-addresses)] + + ```ruby + # in your mailer class + default from: 'Your Name ' + ``` + + * + Make sure that the e-mail delivery method for your test environment is set to + `test`: + [[link](#delivery-method-test)] + + ```ruby + # config/environments/test.rb + + config.action_mailer.delivery_method = :test + ``` + + * + The delivery method for development and production should be `smtp`: + [[link](#delivery-method-smtp)] + + ```ruby + # config/environments/development.rb, config/environments/production.rb + + config.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](https://github.com/fphilipe/premailer-rails) and + [roadie](https://github.com/Mange/roadie). + [[link](#inline-email-styles)] + + * + 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 + of [sidekiq](https://github.com/mperham/sidekiq) gem. + [[link](#background-email)] ## Active Support Core Extensions -* - Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`. -[[link](#try-bang)] + * + Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`. + [[link](#try-bang)] ```ruby # bad @@ -1382,9 +1382,9 @@ obj.try! :fly obj&.fly ``` -* - Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. -[[link](#active_support_aliases)] + * + Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. + [[link](#active_support_aliases)] ```ruby # bad @@ -1396,9 +1396,9 @@ obj&.fly 'the day'.end_with? 'ay' ``` -* - Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. -[[link](#active_support_extensions)] + * + Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. + [[link](#active_support_extensions)] ```ruby # bad @@ -1412,9 +1412,9 @@ obj&.fly 'the day'.include? 'day' ``` -* - Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, and `String#inquiry`. -[[link](#inquiry)] + * + Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, and `String#inquiry`. + [[link](#inquiry)] ```ruby # bad - String#inquiry @@ -1436,118 +1436,118 @@ pets.include? 'cat' ## Time -* - Config your timezone accordingly in `application.rb`. -[[link](#tz-config)] + * + Config your timezone accordingly in `application.rb`. + [[link](#tz-config)] - ```ruby - config.time_zone = 'Eastern European Time' - # optional - note it can be only :utc or :local (default is :utc) - config.active_record.default_timezone = :local - ``` + ```ruby + 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 use `Time.parse`. -[[link](#time-parse)] + * + Don't use `Time.parse`. + [[link](#time-parse)] - ```ruby - # bad - Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone. + ```ruby + # bad + Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone. - # good - Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 - ``` + # good + Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 + ``` -* - Don't use [`String#to_time`](https://apidock.com/rails/String/to_time) -[[link](#to-time)] + * + Don't use [`String#to_time`](https://apidock.com/rails/String/to_time) + [[link](#to-time)] - ```ruby - # bad - assumes time string given is in the system's time zone. - '2015-03-02 19:05:37'.to_time + ```ruby + # bad - assumes time string given is in the system's time zone. + '2015-03-02 19:05:37'.to_time - # good - Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 - ``` + # good + Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 + ``` -* - Don't use `Time.now`. -[[link](#time-now)] + * + Don't use `Time.now`. + [[link](#time-now)] - ```ruby - # bad - Time.now # => Returns system time and ignores your configured time zone. + ```ruby + # bad + Time.now # => Returns system time and ignores your configured time zone. - # good - Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 - Time.current # Same thing but shorter. - ``` + # good + Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 + Time.current # Same thing but shorter. + ``` ## Bundler -* - Put gems used only for development or testing in the appropriate group in the - Gemfile. -[[link](#dev-test-gems)] - -* - 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. -[[link](#only-good-gems)] - -* - OS-specific gems will by default result in a constantly changing - `Gemfile.lock` for projects with multiple developers using different operating - systems. Add all OS X specific gems to a `darwin` group in the Gemfile, and - all Linux specific gems to a `linux` group: -[[link](#os-specific-gemfile-locks)] - - ```ruby - # Gemfile - group :darwin do - gem 'rb-fsevent' - gem 'growl' - end + * + Put gems used only for development or testing in the appropriate group in the + Gemfile. + [[link](#dev-test-gems)] - group :linux do - gem 'rb-inotify' - end - ``` + * + 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. + [[link](#only-good-gems)] + + * + OS-specific gems will by default result in a constantly changing + `Gemfile.lock` for projects with multiple developers using different operating + systems. Add all OS X specific gems to a `darwin` group in the Gemfile, and + all Linux specific gems to a `linux` group: + [[link](#os-specific-gemfile-locks)] + + ```ruby + # Gemfile + group :darwin do + gem 'rb-fsevent' + gem 'growl' + end - To require the appropriate gems in the right environment, add the - following to `config/application.rb`: + group :linux do + gem 'rb-inotify' + end + ``` + + To require the appropriate gems in the right environment, add the + following to `config/application.rb`: - ```ruby - platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym - Bundler.require(platform) - ``` + ```ruby + platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym + Bundler.require(platform) + ``` -* - Do not remove the `Gemfile.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 a `bundle install`. -[[link](#gemfile-lock)] + * + Do not remove the `Gemfile.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 a `bundle install`. + [[link](#gemfile-lock)] ## Managing processes -* - If your projects depends on various external processes use - [foreman](https://github.com/ddollar/foreman) to manage them. -[[link](#foreman)] + * + If your projects depends on various external processes use + [foreman](https://github.com/ddollar/foreman) to manage them. + [[link](#foreman)] # Further Reading There are a few excellent resources on Rails style, that you should consider if you have time to spare: -* [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275) -* [Ruby on Rails Guides](http://guides.rubyonrails.org/) -* [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book) -* [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book) -* [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) -* [Rails 4 Test Prescriptions](https://pragprog.com/book/nrtest2/rails-4-test-prescriptions) -* [Better Specs for RSpec](http://betterspecs.org) + * [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275) + * [Ruby on Rails Guides](http://guides.rubyonrails.org/) + * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book) + * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book) + * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) + * [Rails 4 Test Prescriptions](https://pragprog.com/book/nrtest2/rails-4-test-prescriptions) + * [Better Specs for RSpec](http://betterspecs.org) # Contributing From 6dc84f465723f758d7cb0f9296c58a8df56af5a4 Mon Sep 17 00:00:00 2001 From: Nikolay Shebanov Date: Tue, 17 May 2016 19:50:29 +0300 Subject: [PATCH 103/191] Avoid using instance variables in partials --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index a3173eda..2f0e71bc 100644 --- a/README.md +++ b/README.md @@ -1132,6 +1132,27 @@ render status: :forbidden Mitigate code duplication by using partial templates and layouts. [[link](#partials)] +* + Avoid using instance variables in partials, pass a local variable to `render` 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. +[[link](#no-instance-variables-in-partials)] + +```erb + + +<%= render 'course_description' %> + +<%= @course.description %> + + + +<%= render 'course_description', course: @course %> + +<%= course.description %> +``` + ## Internationalization * From 57841e1b9cb50557d2c3f4d381b9d2c4397f0a51 Mon Sep 17 00:00:00 2001 From: Iago Silva Date: Tue, 19 Feb 2019 12:05:27 -0300 Subject: [PATCH 104/191] Use "find(id)" instead of "find_by(id: id)" "find_by(id: id)" was used as good practice --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f0e71bc..fb38eb81 100644 --- a/README.md +++ b/README.md @@ -843,7 +843,7 @@ render status: :forbidden User.find_by_first_name_and_last_name('Bruce', 'Wayne') # good - User.find_by(id: id) + User.find(id) User.find_by(first_name: 'Bruce', last_name: 'Wayne') ``` From 1b87a0af305c8ccbd344b07ad31cd25e103f5549 Mon Sep 17 00:00:00 2001 From: Iago Silva Date: Tue, 19 Feb 2019 12:40:24 -0300 Subject: [PATCH 105/191] Use "find_by(email: email)" instead of "find(id)" Use "find_by(email: email)" instead of "find(id)" as good practice example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb38eb81..1aa8682c 100644 --- a/README.md +++ b/README.md @@ -843,7 +843,7 @@ render status: :forbidden User.find_by_first_name_and_last_name('Bruce', 'Wayne') # good - User.find(id) + User.find_by(email: email) User.find_by(first_name: 'Bruce', last_name: 'Wayne') ``` From 27c7b26f0de1e2638b793965620d66530b0f8916 Mon Sep 17 00:00:00 2001 From: Iago Silva Date: Tue, 19 Feb 2019 13:23:19 -0300 Subject: [PATCH 106/191] Adjusts find-by-email corresponding bad examples --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1aa8682c..620249d0 100644 --- a/README.md +++ b/README.md @@ -834,11 +834,11 @@ render status: :forbidden ```ruby # bad - User.where(id: id).take + User.where(email: email).take User.where(first_name: 'Bruce', last_name: 'Wayne').take # bad - User.find_by_id(id) + User.find_by_email(email) # bad, deprecated in ActiveRecord 4.0, removed in 4.1+ User.find_by_first_name_and_last_name('Bruce', 'Wayne') From a5b16e9791ab923e4c87fad51be6d5c42967467c Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Thu, 28 Feb 2019 14:18:47 -0500 Subject: [PATCH 107/191] Change language for validation styles This matches the wording used in the `Rails/Validation` cop. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 620249d0..65f1bd33 100644 --- a/README.md +++ b/README.md @@ -481,10 +481,10 @@ render status: :forbidden end ``` - * - Always use the new ["sexy" + * + Always use the ["new-style" validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). - [[link](#sexy-validations)] + [[link](#new-style-validations)] ```ruby # bad From e3ca337adf1face959e3bbd1acce629baf425a7f Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Mon, 22 Apr 2019 09:27:23 -0400 Subject: [PATCH 108/191] Update book links --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 65f1bd33..d0dff0d8 100644 --- a/README.md +++ b/README.md @@ -1562,12 +1562,12 @@ pets.include? 'cat' There are a few excellent resources on Rails style, that you should consider if you have time to spare: - * [The Rails 4 Way](http://www.amazon.com/The-Rails-Addison-Wesley-Professional-Ruby/dp/0321944275) - * [Ruby on Rails Guides](http://guides.rubyonrails.org/) - * [The RSpec Book](http://pragprog.com/book/achbd/the-rspec-book) - * [The Cucumber Book](http://pragprog.com/book/hwcuc/the-cucumber-book) + * [The Rails 5 Way](https://www.informit.com/store/rails-5-way-9780134657677) + * [Ruby on Rails Guides](https://guides.rubyonrails.org/) + * [Effective Testing with RSpec 3](https://pragprog.com/book/rspec3/effective-testing-with-rspec-3) + * [The Cucumber Book](https://pragprog.com/book/hwcuc/the-cucumber-book) * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) - * [Rails 4 Test Prescriptions](https://pragprog.com/book/nrtest2/rails-4-test-prescriptions) + * [Rails 5 Test Prescriptions](https://pragprog.com/book/nrtest3/rails-5-test-prescriptions) * [Better Specs for RSpec](http://betterspecs.org) # Contributing From 54e4536fda7449f2c9a67795ac93dc03966dc363 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Mon, 22 Apr 2019 19:17:47 -0400 Subject: [PATCH 109/191] Update advice for shared instance variables Saying 'no more than two' seems very arbitrary. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d0dff0d8..bb703b76 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ programming resources. [[link](#one-method)] * - Share no more than two instance variables between a controller and a view. + Minimize the number of instance variables passed between a controller and a view. [[link](#shared-instance-variables)] * From bde0f0551c445e16a0b03f4b53272cd940b342e8 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Mon, 22 Apr 2019 19:07:42 -0400 Subject: [PATCH 110/191] Update advice for complex view formatting --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb703b76..e63aedc0 100644 --- a/README.md +++ b/README.md @@ -1124,8 +1124,9 @@ render status: :forbidden [[link](#no-direct-model-view)] * - Never make complex formatting in the views, export the formatting to a method - in the view helper or the model. + 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. [[link](#no-complex-view-formatting)] * From cf96230e797f792006cf8b0f60662eb7ea3cc6c0 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Mon, 22 Apr 2019 12:38:14 -0400 Subject: [PATCH 111/191] Remove section about mailer paths I think this advice doesn't belong in the style guide. Using _path instead of _url is simply a bug, it's not a matter of style or preference. --- README.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/README.md b/README.md index e63aedc0..a2e450a9 100644 --- a/README.md +++ b/README.md @@ -1327,22 +1327,6 @@ your application. default_url_options[:host] = 'your_site.com' ``` - * - If you need to use a link to your site in an email, always use the `_url`, not - `_path` methods. The `_url` methods include the host name and the `_path` - methods don't. - [[link](#url-not-path-in-email)] - - ```ruby - # bad - You can always find more info about this course - <%= link_to 'here', course_path(@course) %> - - # good - You can always find more info about this course - <%= link_to 'here', course_url(@course) %> - ``` - * Format the from and to addresses properly. Use the following format: [[link](#email-addresses)] From 6fc90afadfc7f556c6ec9a76a20fb3045ef18101 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Fri, 26 Apr 2019 23:02:33 -0400 Subject: [PATCH 112/191] Refine callbacks order example Since the linked page already lists the full set of callbacks and their order, I don't think it's necessary to duplicate that in the style guide. --- README.md | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index a2e450a9..375117a7 100644 --- a/README.md +++ b/README.md @@ -578,34 +578,14 @@ render status: :forbidden ```Ruby #bad class Person - after_commit/after_rollback :after_commit_callback - after_save :after_save_callback - around_save :around_save_callback - after_update :after_update_callback - before_update :before_update_callback - after_validation :after_validation_callback + after_commit :after_commit_callback before_validation :before_validation_callback - before_save :before_save_callback - before_create :before_create_callback - after_create :after_create_callback - around_create :around_create_callback - around_update :around_update_callback end #good class Person before_validation :before_validation_callback - after_validation :after_validation_callback - before_save :before_save_callback - around_save :around_save_callback - before_create :before_create_callback - around_create :around_create_callback - after_create :after_create_callback - before_update :before_update_callback - around_update :around_update_callback - after_update :after_update_callback - after_save :after_save_callback - after_commit/after_rollback :after_commit_callback + after_commit :after_commit_callback end ``` From a867dc4ff2443f429ab9acd6e69ca1d46e07f5aa Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Fri, 26 Apr 2019 23:08:52 -0400 Subject: [PATCH 113/191] Update links to HTTPS Update links to HTTPS (where possible) --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a2e450a9..938bbf5b 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ complementary guide to the already existing community-driven Some of the advice here is applicable only to Rails 4.0+. You can generate a PDF or an HTML copy of this guide using -[Pandoc](http://pandoc.org/). +[Pandoc](https://pandoc.org/). Translations of the guide are available in the following languages: @@ -572,7 +572,7 @@ render status: :forbidden * Order callback declarations in the order, in which they will be executed. For - referenece, see [Available Callbacks](http://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks) + referenece, see [Available Callbacks](https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks) [[link](#callbacks-order)] ```Ruby @@ -611,7 +611,7 @@ render status: :forbidden * Beware of the behavior of the - [following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations) + [following](https://guides.rubyonrails.org/active_record_validations.html#skipping-validations) methods. They do not run the model validations and could easily corrupt the model state. [[link](#beware-skip-model-validations)] @@ -1059,7 +1059,7 @@ render status: :forbidden * Name your foreign keys explicitly instead of relying on Rails auto-generated - FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) + FK names. (https://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) [[link](#meaningful-foreign-key-naming)] ```ruby @@ -1081,7 +1081,7 @@ render status: :forbidden * Don't use non-reversible migration commands in the `change` method. Reversible migration commands are listed below. - [ActiveRecord::Migration::CommandRecorder](http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html) + [ActiveRecord::Migration::CommandRecorder](https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html) [[link](#reversible-migration)] ```ruby @@ -1107,7 +1107,7 @@ render status: :forbidden # good # In this case, block will be used by create_table in rollback - # http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table + # https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table class DropUsers < ActiveRecord::Migration def change drop_table :users do |t| @@ -1242,12 +1242,12 @@ render status: :forbidden * More detailed information about the Rails I18n can be found in the [Rails - Guides](http://guides.rubyonrails.org/i18n.html) + Guides](https://guides.rubyonrails.org/i18n.html) [[link](#i18n-guides)] ## Assets -Use the [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within +Use the [assets pipeline](https://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within your application. * @@ -1260,8 +1260,8 @@ your application. [[link](#lib-assets)] * - Third party code such as [jQuery](http://jquery.com/) or - [bootstrap](http://twitter.github.com/bootstrap/) should be placed in + Third party code such as [jQuery](https://jquery.com/) or + [bootstrap](https://twitter.github.com/bootstrap/) should be placed in `vendor/assets`. [[link](#vendor-assets)] @@ -1573,9 +1573,9 @@ It's easy, just follow the [contribution guidelines](https://github.com/rubocop- # License -![Creative Commons License](http://i.creativecommons.org/l/by/3.0/88x31.png) +![Creative Commons License](https://i.creativecommons.org/l/by/3.0/88x31.png) This work is licensed under a [Creative Commons Attribution 3.0 Unported -License](http://creativecommons.org/licenses/by/3.0/deed.en_US) +License](https://creativecommons.org/licenses/by/3.0/deed.en_US) # Spread the Word From cfc0069349c52104304a2a40dec85324daa15ba9 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Wed, 1 May 2019 22:14:50 -0400 Subject: [PATCH 114/191] Replace apidock.com links with official docs As mentioned by @pirj in #246, since apidock is no longer updated. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 938bbf5b..6b6287a3 100644 --- a/README.md +++ b/README.md @@ -907,7 +907,7 @@ render status: :forbidden SQL ``` - [`String#squish`](http://apidock.com/rails/String/squish) removes the indentation and newline characters so that your server + [`String#squish`](https://api.rubyonrails.org/classes/String.html#method-i-squish) removes the indentation and newline characters so that your server log shows a fluid string of SQL rather than something like this: ``` @@ -1465,7 +1465,7 @@ pets.include? 'cat' ``` * - Don't use [`String#to_time`](https://apidock.com/rails/String/to_time) + Don't use [`String#to_time`](https://api.rubyonrails.org/classes/String.html#method-i-to_time) [[link](#to-time)] ```ruby From 7e326ac61cbca855d371e7fe3b444ac2ea2e74f2 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sun, 19 May 2019 15:41:54 +0900 Subject: [PATCH 115/191] Fix a typo in README.md This PR fixes a typo using misspell. https://github.com/client9/misspell ```console % misspell -w . README.md:575:4: corrected "referenece" to "reference" ``` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 389caf3d..0d17237c 100644 --- a/README.md +++ b/README.md @@ -572,7 +572,7 @@ render status: :forbidden * Order callback declarations in the order, in which they will be executed. For - referenece, see [Available Callbacks](https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks) + reference, see [Available Callbacks](https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks) [[link](#callbacks-order)] ```Ruby From e8483bef0661b5d77507c4103fa96e1ffcca39b0 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sun, 2 Jun 2019 11:35:44 +0300 Subject: [PATCH 116/191] Convert the guide to AsciiDoc (#250) AsciiDoc allows us to have native admonitions, footnotes, sane nesting of list items, etc and to easily export the guide in various formats. That would also make it trivial to publish a simple site from the guide down the road if we want to. A longer term plan is to convert all guides to AsciiDoc and maybe even RuboCop's own documentation. RSpec style guide [has been recently converted](https://github.com/rubocop-hq/rspec-style-guide/pull/97). --- .gitignore | 2 + CONTRIBUTING.md | 4 - README.adoc | 1542 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1568 ----------------------------------------------- 4 files changed, 1544 insertions(+), 1572 deletions(-) create mode 100644 .gitignore delete mode 100644 CONTRIBUTING.md create mode 100644 README.adoc delete mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..71d9c3e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +README.html +README.pdf diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index c891b570..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,4 +0,0 @@ -* [Fork](https://help.github.com/articles/fork-a-repo) the project on GitHub. -* Make your feature addition or bug fix in a feature branch. (Include a description of your changes) -* Push your feature branch to GitHub. -* Send a [Pull Request](https://help.github.com/articles/using-pull-requests). \ No newline at end of file diff --git a/README.adoc b/README.adoc new file mode 100644 index 00000000..f3116397 --- /dev/null +++ b/README.adoc @@ -0,0 +1,1542 @@ += The Rails Style Guide +:idprefix: +:idseparator: - +:sectanchors: +:sectlinks: +:toc: preamble +:toclevels: 1 +ifndef::backend-pdf[] +:toc-title: pass:[

Table of Contents

] +endif::[] + +[quote] +____ +Role models are important. + +-- Officer Alex J. Murphy / RoboCop +____ + +The goal of this guide is to present a set of best practices and style prescriptions for Ruby on Rails 4 development. +It's a complementary guide to the already existing community-driven https://github.com/rubocop-hq/ruby-style-guide[Ruby coding style guide]. + +Some of the advice here is applicable only to Rails 4.0+. + +You can generate a PDF copy of this guide using https://asciidoctor.org/docs/asciidoctor-pdf/[AsciiDoctor PDF], and an HTML copy https://asciidoctor.org/docs/convert-documents/#converting-a-document-to-html[with] https://asciidoctor.org/#installation[AsciiDoctor] using the following commands: + +[source,shell] +---- +# Generates README.pdf +asciidoctor-pdf -a allow-uri-read README.adoc + +# Generates README.html +asciidoctor +---- + +Translations of the guide are available in the following languages: + +* https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md[Chinese Simplified] +* https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md[Chinese Traditional] +* https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md[Japanese] +* https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md[Russian] +* https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md[Turkish] +* https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md[Korean] +* https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md[Vietnamese] +* https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md[Portuguese (pt-BR)] + +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. + +== Configuration + +=== Config initializers [[config-initializers]] + +Put custom initialization code in `config/initializers`. +The code in initializers executes on application startup. + +=== Gem initializers [[gem-initializers]] + +Keep initialization code for each gem in a separate file with the same name as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc. + +=== Dev/test/prod configs [[dev-test-prod-configs]] + +Adjust accordingly the settings for development, test and production environment (in the corresponding files under `config/environments/`) + +Mark additional assets for precompilation (if any): + +[source,ruby] +---- +# 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.css rails_admin/rails_admin.js ) +---- + +=== App config [[app-config]] + +Keep configuration that's applicable to all environments in the `config/application.rb` file. + +=== Staging like prod [[staging-like-prod]] + +Create an additional `staging` environment that closely resembles the `production` one. + +=== Yaml config [[yaml-config]] + +Keep any additional configuration in YAML files under the `config/` directory. + +Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method: + +[source,ruby] +---- +Rails::Application.config_for(:yaml_file) +---- + +== Routing + +=== Member collection routes [[member-collection-routes]] + +When you need to add more actions to a RESTful resource (do you really need them at all?) use `member` and `collection` routes. + +[source,ruby] +---- +# bad +get 'subscriptions/:id/unsubscribe' +resources :subscriptions + +# good +resources :subscriptions do + get 'unsubscribe', on: :member +end + +# bad +get 'photos/search' +resources :photos + +# good +resources :photos do + get 'search', on: :collection +end +---- + +=== Many member collection routes [[many-member-collection-routes]] + +If you need to define multiple `member/collection` routes use the alternative block syntax. + +[source,ruby] +---- +resources :subscriptions do + member do + get 'unsubscribe' + # more routes + end +end + +resources :photos do + collection do + get 'search' + # more routes + end +end +---- + +=== Nested routes [[nested-routes]] + +Use nested routes to express better the relationship between ActiveRecord models. + +[source,ruby] +---- +class Post < ActiveRecord::Base + has_many :comments +end + +class Comment < ActiveRecord::Base + belongs_to :post +end + +# routes.rb +resources :posts do + resources :comments +end +---- + +=== Shallow routes [[shallow-routes]] + +If you need to nest routes more than 1 level deep then use the `shallow: true` option. +This will save user from long URLs `posts/1/comments/5/versions/7/edit` and you from long URL helpers `edit_post_comment_version`. + +[source,ruby] +---- +resources :posts, shallow: true do + resources :comments do + resources :versions + end +end +---- + +=== Namespaced routes [[namespaced-routes]] + +Use namespaced routes to group related actions. + +[source,ruby] +---- +namespace :admin do + # Directs /admin/products/* to Admin::ProductsController + # (app/controllers/admin/products_controller.rb) + resources :products +end +---- + +=== No wild routes [[no-wild-routes]] + +Never use the legacy wild controller route. +This route will make all actions in every controller accessible via GET requests. + +[source,ruby] +---- +# very bad +match ':controller(/:action(/:id(.:format)))' +---- + +=== No match routes [[no-match-routes]] + +Don't use `match` 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. + +== Controllers + +=== Skinny controllers [[skinny-controllers]] + +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). + +=== One method [[one-method]] + +Each controller action should (ideally) invoke only one method other than an initial find or new. + +=== Shared instance variables [[shared-instance-variables]] + +Minimize the number of instance variables passed between a controller and a view. + +=== Lexically scoped action filter [[lexically-scoped-action-filter]] + +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. + +[source,ruby] +---- +# bad +class UsersController < ApplicationController + before_action :require_login, only: :export +end + +# good +class UsersController < ApplicationController + before_action :require_login, only: :export + + def export + end +end +---- + +== Controllers: rendering [[rendering]] + +=== Inline rendering [[inline-rendering]] + +Prefer using a template over inline rendering. + +[source,ruby] +---- +# very bad +class ProductsController < ApplicationController + def index + render inline: "<% products.each do |p| %>

<%= p.name %>

<% end %>", type: :erb + end +end + +# good +## app/views/products/index.html.erb +<%= render partial: 'product', collection: products %> + +## app/views/products/_product.html.erb +

<%= product.name %>

+

<%= product.price %>

+ +## app/controllers/foo_controller.rb +class ProductsController < ApplicationController + def index + render :index + end +end +---- + +=== Plain text rendering [[plain-text-rendering]] + +Prefer `render plain:` over `render text:`. + +[source,ruby] +---- +# bad - sets MIME type to `text/html` +... +render text: 'Ruby!' +... + +# bad - requires explicit MIME type declaration +... +render text: 'Ruby!', content_type: 'text/plain' +... + +# good - short and precise +... +render plain: 'Ruby!' +... +---- + +=== HTTP status code symbols [[http-status-code-symbols]] + +Prefer https://gist.github.com/mlanett/a31c340b132ddefa9cca[corresponding symbols] to numeric HTTP status codes. +They are meaningful and do not look like "magic" numbers for less known HTTP status codes. + +[source,ruby] +---- +# bad +... +render status: 403 +... + +# good +... +render status: :forbidden +... +---- + +== Models + +=== Model classes [[model-classes]] + +Introduce non-ActiveRecord model classes freely. + +=== Meaningful model names [[meaningful-model-names]] + +Name the models with meaningful (but short) names without abbreviations. + +=== ActiveAttr gem [[activeattr-gem]] + +If you need model objects that support ActiveRecord behavior (like validation) without the ActiveRecord database functionality use the https://github.com/cgriego/active_attr[ActiveAttr] gem. + +[source,ruby] +---- +class Message + include ActiveAttr::Model + + attribute :name + attribute :email + attribute :content + attribute :priority + + attr_accessible :name, :email, :content + + validates :name, presence: true + validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } + validates :content, length: { maximum: 500 } +end +---- + +For a more complete example refer to the http://railscasts.com/episodes/326-activeattr[RailsCast on the subject]. + +=== Model business logic [[model-business-logic]] + +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. + +== Models: ActiveRecord [[activerecord]] + +=== Keep ActiveRecord defaults [[keep-ar-defaults]] + +Avoid altering ActiveRecord defaults (table names, primary key, etc) unless you have a very good reason (like a database that's not under your control). + +[source,ruby] +---- +# bad - don't do this if you can modify the schema +class Transaction < ActiveRecord::Base + self.table_name = 'order' + ... +end +---- + +=== Macro style methods [[macro-style-methods]] + +Group macro-style methods (`has_many`, `validates`, etc) in the beginning of the class definition. + +[source,ruby] +---- +class User < ActiveRecord::Base + # keep the default scope first (if any) + default_scope { where(active: true) } + + # constants come up next + COLORS = %w(red green blue) + + # afterwards we put attr related macros + attr_accessor :formatted_date_of_birth + + attr_accessible :login, :first_name, :last_name, :email, :password + + # Rails 4+ enums after attr macros, prefer the hash syntax + enum gender: { female: 0, male: 1 } + + # followed by association macros + belongs_to :country + + has_many :authentications, dependent: :destroy + + # and validation macros + validates :email, presence: true + validates :username, presence: true + validates :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 callbacks + before_save :cook + before_save :update_username_lower + + # other macros (like devise's) should be placed after the callbacks + + ... +end +---- + +=== Has many through [[has-many-through]] + +Prefer `has_many :through` to `has_and_belongs_to_many`. +Using `has_many :through` allows additional attributes and validations on the join model. + +[source,ruby] +---- +# not so good - using has_and_belongs_to_many +class User < ActiveRecord::Base + has_and_belongs_to_many :groups +end + +class Group < ActiveRecord::Base + has_and_belongs_to_many :users +end + +# preferred way - using has_many :through +class User < ActiveRecord::Base + has_many :memberships + has_many :groups, through: :memberships +end + +class Membership < ActiveRecord::Base + belongs_to :user + belongs_to :group +end + +class Group < ActiveRecord::Base + has_many :memberships + has_many :users, through: :memberships +end +---- + +=== Read attribute [[read-attribute]] + +Prefer `self[:attribute]` over `read_attribute(:attribute)`. + +[source,ruby] +---- +# bad +def amount + read_attribute(:amount) * 100 +end + +# good +def amount + self[:amount] * 100 +end +---- + +=== Write attribute [[write-attribute]] + +Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. + +[source,ruby] +---- +# bad +def amount + write_attribute(:amount, 100) +end + +# good +def amount + self[:amount] = 100 +end +---- + +=== New style validations [[new-style-validations]] + +Always use the http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/["new-style" validations]. + +[source,ruby] +---- +# bad +validates_presence_of :email +validates_length_of :email, maximum: 100 + +# good +validates :email, presence: true, length: { maximum: 100 } +---- + +=== Single attribute validations [[single-attribute-validations]] + +To make validations easy to read, don't list multiple attributes per validation. + +[source,ruby] +---- +# bad +validates :email, :password, presence: true +validates :email, length: { maximum: 100 } + +# good +validates :email, presence: true, length: { maximum: 100 } +validates :password, presence: true +---- + +=== Custom validator file [[custom-validator-file]] + +When a custom validation is used more than once or the validation is some regular expression mapping, create a custom validator file. + +[source,ruby] +---- +# bad +class Person + validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } +end + +# good +class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i + end +end + +class Person + validates :email, email: true +end +---- + +=== App validators [[app-validators]] + +Keep custom validators under `app/validators`. + +=== Custom validators gem [[custom-validators-gem]] + +Consider extracting custom validators to a shared gem if you're maintaining several related apps or the validators are generic enough. + +=== Named scopes [[named-scopes]] + +Use named scopes freely. + +[source,ruby] +---- +class User < ActiveRecord::Base + scope :active, -> { where(active: true) } + scope :inactive, -> { where(active: false) } + + scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } +end +---- + +=== Named scope class [[named-scope-class]] + +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 an `ActiveRecord::Relation` object. +Arguably you can define even simpler scopes like this. + +[source,ruby] +---- +class User < ActiveRecord::Base + def self.with_orders + joins(:orders).select('distinct(users.id)') + end +end +---- + +=== Callbacks order [[callbacks-order]] + +Order callback declarations in the order, in which they will be executed. +For reference, see https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks[Available Callbacks]. + +[source,Ruby] +---- +#bad +class Person + after_commit :after_commit_callback + before_validation :before_validation_callback +end + +#good +class Person + before_validation :before_validation_callback + after_commit :after_commit_callback +end +---- + +=== Beware skip model validations [[beware-skip-model-validations]] + +Beware of the behavior of the https://guides.rubyonrails.org/active_record_validations.html#skipping-validations[following] methods. +They do not run the model validations and could easily corrupt the model state. + +[source,ruby] +---- +# bad +Article.first.decrement!(:view_count) +DiscussionBoard.decrement_counter(:post_count, 5) +Article.first.increment!(:view_count) +DiscussionBoard.increment_counter(:post_count, 5) +person.toggle :active +product.touch +Billing.update_all("category = 'authorized', author = 'David'") +user.update_attribute(:website, 'example.com') +user.update_columns(last_request_at: Time.current) +Post.update_counters 5, comment_count: -1, action_count: 1 + +# good +user.update_attributes(website: 'example.com') +---- + +=== User friendly URLs [[user-friendly-urls]] + +Use user-friendly URLs. +Show some descriptive attribute of the model in the URL rather than its `id`. +There is more than one way to achieve this. + +==== Override the `to_param` method of the model + +This method is used by Rails for constructing a URL to the object. +The default implementation returns the `id` of the record as a String. +It could be overridden to include another human-readable attribute. + +[source,ruby] +---- +class Person + def to_param + "#{id} #{name}".parameterize + end +end +---- + +In order to convert this to a URL-friendly value, `parameterize` should be called on the string. +The `id` of the object needs to be at the beginning so that it can be found by the `find` method of ActiveRecord. + +==== `friendly_id` gem + +It allows creation of human-readable URLs by using some descriptive attribute of the model instead of its `id`. + +[source,ruby] +---- +class Person + extend FriendlyId + friendly_id :name, use: :slugged +end +---- + +Check the https://github.com/norman/friendly_id[gem documentation] for more information about its usage. + +=== `find_each` [[find-each]] + +Use `find_each` to iterate over a collection of AR objects. +Looping through a collection of records from the database (using the `all` 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. + +[source,ruby] +---- +# bad +Person.all.each do |person| + person.do_awesome_stuff +end + +Person.where('age > 21').each do |person| + person.party_all_night! +end + +# good +Person.find_each do |person| + person.do_awesome_stuff +end + +Person.where('age > 21').find_each do |person| + person.party_all_night! +end +---- + +=== `before_destroy` [[before_destroy]] + +Since https://github.com/rails/rails/issues/3458[Rails creates callbacks for dependent associations], always call `before_destroy` callbacks that perform validation with `prepend: true`. + +[source,ruby] +---- +# bad (roles will be deleted automatically even if super_admin? is true) +has_many :roles, dependent: :destroy + +before_destroy :ensure_deletable + +def ensure_deletable + raise "Cannot delete super admin." if super_admin? +end + +# good +has_many :roles, dependent: :destroy + +before_destroy :ensure_deletable, prepend: true + +def ensure_deletable + raise "Cannot delete super admin." if super_admin? +end +---- + +=== `has_many`/`has_one` dependent option [[has_many-has_one-dependent-option]] + +Define the `dependent` option to the `has_many` and `has_one` associations. + +[source,ruby] +---- +# bad +class Post < ActiveRecord::Base + has_many :comments +end + +# good +class Post < ActiveRecord::Base + has_many :comments, dependent: :destroy +end +---- + +=== `save!` [[save-bang]] + +When persisting AR objects always use the exception raising bang! method or handle the method return value. +This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. + +[source,ruby] +---- +# bad +user.create(name: 'Bruce') + +# bad +user.save + +# good +user.create!(name: 'Bruce') +# or +bruce = user.create(name: 'Bruce') +if bruce.persisted? + ... +else + ... +end + +# good +user.save! +# or +if user.save + ... +else + ... +end +---- + +== Models: ActiveRecord Queries [[activerecord-queries]] + +=== Avoid interpolation [[avoid-interpolation]] + +Avoid string interpolation in queries, as it will make your code susceptible to SQL injection attacks. + +[source,ruby] +---- +# bad - param will be interpolated unescaped +Client.where("orders_count = #{params[:orders]}") + +# good - param will be properly escaped +Client.where('orders_count = ?', params[:orders]) +---- + +=== Named placeholder [[named-placeholder]] + +Consider using named placeholders instead of positional placeholders when you have more than 1 placeholder in your query. + +[source,ruby] +---- +# okish +Client.where( + 'created_at >= ? AND created_at <= ?', + params[:start_date], params[:end_date] +) + +# good +Client.where( + 'created_at >= :start_date AND created_at <= :end_date', + start_date: params[:start_date], end_date: params[:end_date] +) +---- + +=== `find` [[find]] + +Favor the use of `find` over `where.take!`, `find_by!`, and `find_by_id!` when you need to retrieve a single record by primary key id and raise `ActiveRecord::RecordNotFound` when the record is not found. + +[source,ruby] +---- +# bad +User.where(id: id).take! + +# bad +User.find_by_id!(id) + +# bad +User.find_by!(id: id) + +# good +User.find(id) +---- + +=== `find_by` [[find_by]] + +Favor the use of `find_by` over `where.take` and `find_by_attribute` when you need to retrieve a single record by one or more attributes and return `nil` when the record is not found. + +[source,ruby] +---- +# bad +User.where(email: email).take +User.where(first_name: 'Bruce', last_name: 'Wayne').take + +# bad +User.find_by_email(email) +# bad, deprecated in ActiveRecord 4.0, removed in 4.1+ +User.find_by_first_name_and_last_name('Bruce', 'Wayne') + +# good +User.find_by(email: email) +User.find_by(first_name: 'Bruce', last_name: 'Wayne') +---- + +=== Where not [[where-not]] + +Favor the use of `where.not` over SQL. + +[source,ruby] +---- +# bad +User.where("id != ?", id) + +# good +User.where.not(id: id) +---- + +=== Order by `id` [[order-by-id]] + +Don't use the `id` 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. + +[source,ruby] +---- +# bad +scope :chronological, -> { order(id: :asc) } + +# good +scope :chronological, -> { order(created_at: :asc) } +---- + +=== `ids` [[ids]] + +Favor the use of `ids` over `pluck(:id)`. + +[source,Ruby] +---- +# bad +User.pluck(:id) + +# good +User.ids +---- + +=== Squished heredocs [[squished-heredocs]] + +When specifying an explicit query in a method such as `find_by_sql`, use heredocs with `squish`. +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). + +[source,ruby] +---- +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 +---- + +https://api.rubyonrails.org/classes/String.html#method-i-squish[`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 acounts\n ON\n accounts.user_id = users.id +---- + +=== `size` over `count` or `length` [[size-over-count-or-length]] + +When querying ActiveRecord collections, prefer `size` (selects between count/length behavior based on whether collection is already loaded) or `length` (always loads the whole collection and counts the array elements) over `count` (always does a database query for the count). + +[source,ruby] +---- +# bad +User.count + +# good +User.all.size + +# good - if you really need to load all users into memory +User.all.length +---- + +== Migrations + +=== Schema version [[schema-version]] + +Keep the `schema.rb` (or `structure.sql`) under version control. + +=== Db schema load [[db-schema-load]] + +Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty database. + +=== Default migration values [[default-migration-values]] + +Enforce default values in the migrations themselves instead of in the application layer. + +[source,ruby] +---- +# bad - application enforced default value +class Product < ActiveRecord::Base + def amount + self[:amount] || 0 + end +end + +# good - database enforced +class AddDefaultAmountToProducts < ActiveRecord::Migration + def change + change_column_default :products, :amount, 0 + end +end +---- + +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. + +=== Foreign key constraints [[foreign-key-constraints]] + +Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord supports foreign key constraints natively. + +=== Change vs up down [[change-vs-up-down]] + +When writing constructive migrations (adding tables or columns), use the `change` method instead of `up` and `down` methods. + +[source,ruby] +---- +# the old way +class AddNameToPeople < ActiveRecord::Migration + def up + add_column :people, :name, :string + end + + def down + remove_column :people, :name + end +end + +# the new preferred way +class AddNameToPeople < ActiveRecord::Migration + def change + add_column :people, :name, :string + end +end +---- + +=== Define model class migrations [[define-model-class-migrations]] + +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. + +[source,ruby] +---- +# db/migrate/.rb +# frozen_string_literal: true + +# bad +class ModifyDefaultStatusForProducts < ActiveRecord::Migration + def change + old_status = 'pending_manual_approval' + new_status = 'pending_approval' + + reversible do |dir| + dir.up do + Product.where(status: old_status).update_all(status: new_status) + change_column :products, :status, :string, default: new_status + end + + dir.down do + Product.where(status: new_status).update_all(status: old_status) + change_column :products, :status, :string, default: old_status + end + end + end +end + +# 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. +class MigrationProduct < ActiveRecord::Base + self.table_name = :products +end + +class ModifyDefaultStatusForProducts < ActiveRecord::Migration + def change + old_status = 'pending_manual_approval' + new_status = 'pending_approval' + + reversible do |dir| + dir.up do + MigrationProduct.where(status: old_status).update_all(status: new_status) + change_column :products, :status, :string, default: new_status + end + + dir.down do + MigrationProduct.where(status: new_status).update_all(status: old_status) + change_column :products, :status, :string, default: old_status + end + end + end +end +---- + +=== Meaningful foreign key naming [[meaningful-foreign-key-naming]] + +Name your foreign keys explicitly instead of relying on Rails auto-generated FK names. (https://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) + +[source,ruby] +---- +# bad +class AddFkArticlesToAuthors < ActiveRecord::Migration + def change + add_foreign_key :articles, :authors + end +end + +# good +class AddFkArticlesToAuthors < ActiveRecord::Migration + def change + add_foreign_key :articles, :authors, name: :articles_author_id_fk + end +end +---- + +=== Reversible migration [[reversible-migration]] + +Don't use non-reversible migration commands in the `change` method. +Reversible migration commands are listed below. +https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html[ActiveRecord::Migration::CommandRecorder] + +[source,ruby] +---- +# bad +class DropUsers < ActiveRecord::Migration + def change + drop_table :users + end +end + +# good +class DropUsers < ActiveRecord::Migration + def up + drop_table :users + end + + def down + create_table :users do |t| + t.string :name + end + end +end + +# good +# In this case, block will be used by create_table in rollback +# https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table +class DropUsers < ActiveRecord::Migration + def change + drop_table :users do |t| + t.string :name + end + end +end +---- + +== Views + +=== No direct model view [[no-direct-model-view]] + +Never call the model layer directly from a view. + +=== No complex view formatting [[no-complex-view-formatting]] + +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. + +=== Partials [[partials]] + +Mitigate code duplication by using partial templates and layouts. + +=== No instance variables in partials [[no-instance-variables-in-partials]] + +Avoid using instance variables in partials, pass a local variable to `render` 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. + +[source,erb] +---- + + +<%= render 'course_description' %> + +<%= @course.description %> + + + +<%= render 'course_description', course: @course %> + +<%= course.description %> +---- + +== Internationalization + +=== Locale texts [[locale-texts]] + +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 the `config/locales` directory. + +=== Translated labels [[translated-labels]] + +When the labels of an ActiveRecord model need to be translated, use the `activerecord` scope: + +---- +en: + activerecord: + models: + user: Member + attributes: + user: + name: 'Full name' +---- + +Then `User.model_name.human` will return "Member" and `User.human_attribute_name("name")` will return "Full name". +These translations of the attributes will be used as labels in the views. + +=== Organize locale files [[organize-locale-files]] + +Separate the texts used in the views from translations of ActiveRecord attributes. +Place the locale files for the models in a folder `locales/models` and the texts used in the views in folder `locales/views`. + +When organization of the locale files is done with additional directories, these directories must be described in the `application.rb` file in order to be loaded. + +[source,ruby] +---- +# config/application.rb +config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] +---- + +=== Shared localization [[shared-localization]] + +Place the shared localization options, such as date or currency formats, in files under the root of the `locales` directory. + +=== Short i18n [[short-i18n]] + +Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` and `I18n.l` instead of `I18n.localize`. + +=== Lazy lookup [[lazy-lookup]] + +Use "lazy" lookup for the texts used in views. Let's say we have the following structure: + +---- +en: + users: + show: + title: 'User details page' +---- + +The value for `users.show.title` can be looked up in the template `app/views/users/show.html.haml` like this: + +[source,ruby] +---- += t '.title' +---- + +=== Dot-separated keys [[dot-separated-keys]] + +Use the dot-separated keys in the controllers and models instead of specifying the `:scope` option. +The dot-separated call is easier to read and trace the hierarchy. + +[source,ruby] +---- +# bad +I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] + +# good +I18n.t 'activerecord.errors.messages.record_invalid' +---- + +=== I18n guides [[i18n-guides]] + +More detailed information about the Rails I18n can be found in the https://guides.rubyonrails.org/i18n.html[Rails Guides] + + +== Assets + +Use the https://guides.rubyonrails.org/asset_pipeline.html[asset pipeline] to leverage organization within your application. + +=== Reserve `app/assets` [[reserve-app-assets]] + +Reserve `app/assets` for custom stylesheets, javascripts, or images. + +=== `lib/assets` [[lib-assets]] + +Use `lib/assets` for your own libraries that don't really fit into the scope of the application. + +=== `vendor/assets` [[vendor-assets]] + +Third party code such as https://jquery.com/[jQuery] or https://twitter.github.com/bootstrap/[bootstrap] should be placed in `vendor/assets`. + +=== `gem/assets` [[gem-assets]] + +When possible, use gemified versions of assets (e.g. https://github.com/rails/jquery-rails[jquery-rails], https://github.com/joliss/jquery-ui-rails[jquery-ui-rails], https://github.com/thomas-mcdonald/bootstrap-sass[bootstrap-sass], https://github.com/zurb/foundation[zurb-foundation]). + +== Mailers + +=== Mailer name [[mailer-name]] + +Name the mailers `SomethingMailer`. +Without the Mailer suffix it isn't immediately apparent what's a mailer and which views are related to the mailer. + +=== HTML plain email [[html-plain-email]] + +Provide both HTML and plain-text view templates. + +=== Enable delivery errors [[enable-delivery-errors]] + +Enable errors raised on failed mail delivery in your development environment. +The errors are disabled by default. + +[source,ruby] +---- +# config/environments/development.rb + +config.action_mailer.raise_delivery_errors = true +---- + +=== Local SMTP [[local-smtp]] + +Use a local SMTP server like https://github.com/sj26/mailcatcher[Mailcatcher] in development environment. + +[source,ruby] +---- +# config/environments/development.rb + +config.action_mailer.smtp_settings = { + address: 'localhost', + port: 1025, + # more settings +} +---- + +=== Default hostname [[default-hostname]] + +Provide default settings for the host name. + +[source,ruby] +---- +# config/environments/development.rb +config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } + +# config/environments/production.rb +config.action_mailer.default_url_options = { host: 'your_site.com' } + +# in your mailer class +default_url_options[:host] = 'your_site.com' +---- + +=== Email addresses [[email-addresses]] + +Format the from and to addresses properly. +Use the following format: + +[source,ruby] +---- +# in your mailer class +default from: 'Your Name ' +---- + +=== Delivery method test [[delivery-method-test]] + +Make sure that the e-mail delivery method for your test environment is set to `test`: + +[source,ruby] +---- +# config/environments/test.rb + +config.action_mailer.delivery_method = :test +---- + +=== Delivery method SMTP [[delivery-method-smtp]] + +The delivery method for development and production should be `smtp`: + +[source,ruby] +---- +# config/environments/development.rb, config/environments/production.rb + +config.action_mailer.delivery_method = :smtp +---- + +=== Inline email styles [[inline-email-styles]] + +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: https://github.com/fphilipe/premailer-rails[premailer-rails] and https://github.com/Mange/roadie[roadie]. + +=== Background email [[background-email]] + +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 of https://github.com/mperham/sidekiq[sidekiq] gem. + +== Active Support Core Extensions + +=== `try!` [[try-bang]] + +Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`. + +[source,ruby] +---- +# bad +obj.try! :fly + +# good +obj&.fly +---- + +=== ActiveSupport aliases [[active_support_aliases]] + +Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. + +[source,ruby] +---- +# bad +'the day'.starts_with? 'th' +'the day'.ends_with? 'ay' + +# good +'the day'.start_with? 'th' +'the day'.end_with? 'ay' +---- + +=== ActiveSupport extensions [[active_support_extensions]] + +Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. + +[source,ruby] +---- +# bad +(1..50).to_a.forty_two +1.in? [1, 2] +'day'.in? 'the day' + +# good +(1..50).to_a[41] +[1, 2].include? 1 +'the day'.include? 'day' +---- + +=== `inquiry` [[inquiry]] + +Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, and `String#inquiry`. + +[source,ruby] +---- +# bad - String#inquiry +ruby = 'two'.inquiry +ruby.two? + +# good +ruby = 'two' +ruby == 'two' + +# bad - Array#inquiry +pets = %w(cat dog).inquiry +pets.gopher? + +# good +pets = %w(cat dog) +pets.include? 'cat' +---- + +== Time + +=== Time zone config [[tz-config]] + +Configure your timezone accordingly in `application.rb`. + +[source,ruby] +---- +config.time_zone = 'Eastern European Time' +# optional - note it can be only :utc or :local (default is :utc) +config.active_record.default_timezone = :local +---- + +=== Time parse [[time-parse]] + +Don't use `Time.parse`. + +[source,ruby] +---- +# bad +Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone. + +# good +Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 +---- + +=== `to_time` [[to-time]] + +Don't use https://api.rubyonrails.org/classes/String.html#method-i-to_time[`String#to_time`] + +[source,ruby] +---- +# bad - assumes time string given is in the system's time zone. +'2015-03-02 19:05:37'.to_time + +# good +Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 +---- + +=== `Time.now` [[time-now]] + +Don't use `Time.now`. + +[source,ruby] +---- +# bad +Time.now # => Returns system time and ignores your configured time zone. + +# good +Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 +Time.current # Same thing but shorter. +---- + +== Bundler + +=== Dev/test gems [[dev-test-gems]] + +Put gems used only for development or testing in the appropriate group in the Gemfile. + +=== Only good gems [[only-good-gems]] + +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. + +=== OS-specific `Gemfile.lock` [[os-specific-gemfile-locks]] + +OS-specific gems will by default result in a constantly changing `Gemfile.lock` for projects with multiple developers using different operating systems. +Add all OS X specific gems to a `darwin` group in the Gemfile, and all Linux specific gems to a `linux` group: + +[source,ruby] +---- +# Gemfile +group :darwin do + gem 'rb-fsevent' + gem 'growl' +end + +group :linux do + gem 'rb-inotify' +end +---- + +To require the appropriate gems in the right environment, add the following to `config/application.rb`: + +[source,ruby] +---- +platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym +Bundler.require(platform) +---- + +=== `Gemfile.lock` [[gemfile-lock]] + +Do not remove the `Gemfile.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 a `bundle install`. + +== Managing processes + +=== Foreman [[foreman]] + +If your projects depends on various external processes use https://github.com/ddollar/foreman[foreman] to manage them. + +== Further Reading + +There are a few excellent resources on Rails style, that you should consider if you have time to spare: + +* https://www.informit.com/store/rails-5-way-9780134657677[The Rails 5 Way] +* https://guides.rubyonrails.org/[Ruby on Rails Guides] +* https://pragprog.com/book/rspec3/effective-testing-with-rspec-3[Effective Testing with RSpec 3] +* https://pragprog.com/book/hwcuc/the-cucumber-book[The Cucumber Book] +* https://leanpub.com/everydayrailsrspec[Everyday Rails Testing with RSpec] +* https://pragprog.com/book/nrtest3/rails-5-test-prescriptions[Rails 5 Test Prescriptions] +* http://betterspecs.org[Better Specs for RSpec] + +== Contributing + +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 via https://www.patreon.com/bbatsov[Patreon]. + +=== How to Contribute? + +It's easy, just follow the contribution guidelines below: + +* https://help.github.com/articles/fork-a-repo[Fork] the project on GitHub +* Make your feature addition or bug fix in a feature branch. +* Include a http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[good description] of your changes +* Push your feature branch to GitHub +* Send a https://help.github.com/articles/using-pull-requests[Pull Request] + +== License + +image:https://i.creativecommons.org/l/by/3.0/88x31.png[Creative Commons License] +This work is licensed under a https://creativecommons.org/licenses/by/3.0/deed.en_US[Creative Commons Attribution 3.0 Unported License] + +== Spread the Word + +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, + +https://twitter.com/bbatsov[Bozhidar] diff --git a/README.md b/README.md deleted file mode 100644 index 0d17237c..00000000 --- a/README.md +++ /dev/null @@ -1,1568 +0,0 @@ -# Prelude - -> Role models are important.
-> -- Officer Alex J. Murphy / RoboCop - -The goal of this guide is to present a set of best practices and style -prescriptions for Ruby on Rails 4 development. It's a -complementary guide to the already existing community-driven -[Ruby coding style guide](https://github.com/rubocop-hq/ruby-style-guide). - -Some of the advice here is applicable only to Rails 4.0+. - -You can generate a PDF or an HTML copy of this guide using -[Pandoc](https://pandoc.org/). - -Translations of the guide are available in the following languages: - - * [Chinese Simplified](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md) - * [Chinese Traditional](https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md) - * [Japanese](https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md) - * [Russian](https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md) - * [Turkish](https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md) - * [Korean](https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md) - * [Vietnamese](https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md) - * [Portuguese (pt-BR)](https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md) - -# The Rails 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. - -## Table of Contents - - * [Configuration](#configuration) - * [Routing](#routing) - * [Controllers](#controllers) - * [Rendering](#rendering) - * [Models](#models) - * [ActiveRecord](#activerecord) - * [ActiveRecord Queries](#activerecord-queries) - * [Migrations](#migrations) - * [Views](#views) - * [Internationalization](#internationalization) - * [Assets](#assets) - * [Mailers](#mailers) - * [Active Support Core Extensions](#active-support-core-extensions) - * [Time](#time) - * [Bundler](#bundler) - * [Managing processes](#managing-processes) - -## Configuration - - * - Put custom initialization code in `config/initializers`. The code in - initializers executes on application startup. - [[link](#config-initializers)] - - * - Keep initialization code for each gem in a separate file with the same name - as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc. - [[link](#gem-initializers)] - - * - Adjust accordingly the settings for development, test and production - environment (in the corresponding files under `config/environments/`) - [[link](#dev-test-prod-configs)] - - * Mark additional assets for precompilation (if any): - - ```ruby - # 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.css rails_admin/rails_admin.js ) - ``` - - * - Keep configuration that's applicable to all environments in the - `config/application.rb` file. - [[link](#app-config)] - - * - Create an additional `staging` environment that closely resembles the - `production` one. - [[link](#staging-like-prod)] - - * - Keep any additional configuration in YAML files under the `config/` directory. - [[link](#yaml-config)] - - Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method: - - ```ruby - Rails::Application.config_for(:yaml_file) - ``` - -## Routing - - * - When you need to add more actions to a RESTful resource (do you really need - them at all?) use `member` and `collection` routes. - [[link](#member-collection-routes)] - - ```ruby - # bad - get 'subscriptions/:id/unsubscribe' - resources :subscriptions - - # good - resources :subscriptions do - get 'unsubscribe', on: :member - end - - # bad - get 'photos/search' - resources :photos - - # good - resources :photos do - get 'search', on: :collection - end - ``` - - * - If you need to define multiple `member/collection` routes use the - alternative block syntax. - [[link](#many-member-collection-routes)] - - ```ruby - resources :subscriptions do - member do - get 'unsubscribe' - # more routes - end - end - - resources :photos do - collection do - get 'search' - # more routes - end - end - ``` - - * - Use nested routes to express better the relationship between ActiveRecord - models. - [[link](#nested-routes)] - - ```ruby - class Post < ActiveRecord::Base - has_many :comments - end - - class Comment < ActiveRecord::Base - belongs_to :post - end - - # routes.rb - resources :posts do - resources :comments - end - ``` - - * - If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long urls `posts/1/comments/5/versions/7/edit` and you from long url helpers `edit_post_comment_version`. - - ```ruby - resources :posts, shallow: true do - resources :comments do - resources :versions - end - end - ``` - - * - Use namespaced routes to group related actions. - [[link](#namespaced-routes)] - - ```ruby - namespace :admin do - # Directs /admin/products/* to Admin::ProductsController - # (app/controllers/admin/products_controller.rb) - resources :products - end - ``` - - * - Never use the legacy wild controller route. This route will make all actions - in every controller accessible via GET requests. - [[link](#no-wild-routes)] - - ```ruby - # very bad - match ':controller(/:action(/:id(.:format)))' - ``` - - * - Don't use `match` 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. - [[link](#no-match-routes)] - -## Controllers - - * - 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). - [[link](#skinny-controllers)] - - * - Each controller action should (ideally) invoke only one method other than an - initial find or new. - [[link](#one-method)] - - * - Minimize the number of instance variables passed between a controller and a view. - [[link](#shared-instance-variables)] - - * - 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. - [[link](#lexically-scoped-action-filter)] - -```ruby -# bad -class UsersController < ApplicationController - before_action :require_login, only: :export -end - -# good -class UsersController < ApplicationController - before_action :require_login, only: :export - - def export - end -end -``` - -### Rendering - - * - Prefer using a template over inline rendering. - [[link](#inline-rendering)] - -```ruby -# very bad -class ProductsController < ApplicationController - def index - render inline: "<% products.each do |p| %>

<%= p.name %>

<% end %>", type: :erb - end -end - -# good -## app/views/products/index.html.erb -<%= render partial: 'product', collection: products %> - -## app/views/products/_product.html.erb -

<%= product.name %>

-

<%= product.price %>

- -## app/controllers/foo_controller.rb -class ProductsController < ApplicationController - def index - render :index - end -end -``` - - * - Prefer `render plain:` over `render text:`. - [[link](#plain-text-rendering)] - -```ruby -# bad - sets MIME type to `text/html` -... -render text: 'Ruby!' -... - -# bad - requires explicit MIME type declaration -... -render text: 'Ruby!', content_type: 'text/plain' -... - -# good - short and precise -... -render plain: 'Ruby!' -... -``` - - * - Prefer [corresponding symbols](https://gist.github.com/mlanett/a31c340b132ddefa9cca) to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes. - [[link](#http-status-code-symbols)] - -```ruby -# bad -... -render status: 403 -... - -# good -... -render status: :forbidden -... -``` - -## Models - - * - Introduce non-ActiveRecord model classes freely. - [[link](#model-classes)] - - * - Name the models with meaningful (but short) names without abbreviations. - [[link](#meaningful-model-names)] - - * - If you need model objects that support ActiveRecord behavior (like validation) - without the ActiveRecord database functionality use the - [ActiveAttr](https://github.com/cgriego/active_attr) gem. - [[link](#activeattr-gem)] - - ```ruby - class Message - include ActiveAttr::Model - - attribute :name - attribute :email - attribute :content - attribute :priority - - attr_accessible :name, :email, :content - - validates :name, presence: true - validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } - validates :content, length: { maximum: 500 } - end - ``` - - For a more complete example refer to the - [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr). - - * - 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. - [[link](#model-business-logic)] - -### ActiveRecord - - * - Avoid altering ActiveRecord defaults (table names, primary key, etc) unless - you have a very good reason (like a database that's not under your control). - [[link](#keep-ar-defaults)] - - ```ruby - # bad - don't do this if you can modify the schema - class Transaction < ActiveRecord::Base - self.table_name = 'order' - ... - end - ``` - - * - Group macro-style methods (`has_many`, `validates`, etc) in the beginning of - the class definition. - [[link](#macro-style-methods)] - - ```ruby - class User < ActiveRecord::Base - # keep the default scope first (if any) - default_scope { where(active: true) } - - # constants come up next - COLORS = %w(red green blue) - - # afterwards we put attr related macros - attr_accessor :formatted_date_of_birth - - attr_accessible :login, :first_name, :last_name, :email, :password - - # Rails4+ enums after attr macros, prefer the hash syntax - enum gender: { female: 0, male: 1 } - - # followed by association macros - belongs_to :country - - has_many :authentications, dependent: :destroy - - # and validation macros - validates :email, presence: true - validates :username, presence: true - validates :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 callbacks - before_save :cook - before_save :update_username_lower - - # other macros (like devise's) should be placed after the callbacks - - ... - end - ``` - - * - Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many - :through` allows additional attributes and validations on the join model. - [[link](#has-many-through)] - - ```ruby - # not so good - using has_and_belongs_to_many - class User < ActiveRecord::Base - has_and_belongs_to_many :groups - end - - class Group < ActiveRecord::Base - has_and_belongs_to_many :users - end - - # preferred way - using has_many :through - class User < ActiveRecord::Base - has_many :memberships - has_many :groups, through: :memberships - end - - class Membership < ActiveRecord::Base - belongs_to :user - belongs_to :group - end - - class Group < ActiveRecord::Base - has_many :memberships - has_many :users, through: :memberships - end - ``` - - * - Prefer `self[:attribute]` over `read_attribute(:attribute)`. - [[link](#read-attribute)] - - ```ruby - # bad - def amount - read_attribute(:amount) * 100 - end - - # good - def amount - self[:amount] * 100 - end - ``` - - * - Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. - [[link](#write-attribute)] - - ```ruby - # bad - def amount - write_attribute(:amount, 100) - end - - # good - def amount - self[:amount] = 100 - end - ``` - - * - Always use the ["new-style" - validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). - [[link](#new-style-validations)] - - ```ruby - # bad - validates_presence_of :email - validates_length_of :email, maximum: 100 - - # good - validates :email, presence: true, length: { maximum: 100 } - ``` - - * - To make validations easy to read, don't list multiple attributes per - validation - [[link](#single-attribute-validations)] - - ```ruby - # bad - validates :email, :password, presence: true - validates :email, length: { maximum: 100 } - - # good - validates :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. - [[link](#custom-validator-file)] - - ```ruby - # bad - class Person - validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } - end - - # good - class EmailValidator < ActiveModel::EachValidator - def validate_each(record, attribute, value) - record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i - end - end - - class Person - validates :email, email: true - end - ``` - - * - Keep custom validators under `app/validators`. - [[link](#app-validators)] - - * - Consider extracting custom validators to a shared gem if you're maintaining - several related apps or the validators are generic enough. - [[link](#custom-validators-gem)] - - * - Use named scopes freely. - [[link](#named-scopes)] - - ```ruby - class User < ActiveRecord::Base - scope :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 an `ActiveRecord::Relation` - object. Arguably you can define even simpler scopes like this. - [[link](#named-scope-class)] - - ```ruby - class User < ActiveRecord::Base - def self.with_orders - joins(:orders).select('distinct(users.id)') - end - end - ``` - - * - Order callback declarations in the order, in which they will be executed. For - reference, see [Available Callbacks](https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks) - [[link](#callbacks-order)] - - ```Ruby - #bad - class Person - after_commit :after_commit_callback - before_validation :before_validation_callback - end - - #good - class Person - before_validation :before_validation_callback - after_commit :after_commit_callback - end - ``` - - * - Beware of the behavior of the - [following](https://guides.rubyonrails.org/active_record_validations.html#skipping-validations) - methods. They do not run the model validations and - could easily corrupt the model state. - [[link](#beware-skip-model-validations)] - - ```ruby - # bad - Article.first.decrement!(:view_count) - DiscussionBoard.decrement_counter(:post_count, 5) - Article.first.increment!(:view_count) - DiscussionBoard.increment_counter(:post_count, 5) - person.toggle :active - product.touch - Billing.update_all("category = 'authorized', author = 'David'") - user.update_attribute(:website, 'example.com') - user.update_columns(last_request_at: Time.current) - Post.update_counters 5, comment_count: -1, action_count: 1 - - # good - user.update_attributes(website: 'example.com') - ``` - - * - Use user-friendly URLs. Show some descriptive attribute of the model in the URL - rather than its `id`. There is more than one way to achieve this: - [[link](#user-friendly-urls)] - - * Override the `to_param` method of the model. This method is used by Rails - for constructing a URL to the object. The default implementation returns - the `id` of the record as a String. It could be overridden to include another - human-readable attribute. - - ```ruby - class Person - def to_param - "#{id} #{name}".parameterize - end - end - ``` - - In order to convert this to a URL-friendly value, `parameterize` should be - called on the string. The `id` of the object needs to be at the beginning so - that it can be found by the `find` method of ActiveRecord. - - * Use the `friendly_id` gem. It allows creation of human-readable URLs by - using some descriptive attribute of the model instead of its `id`. - - ```ruby - class Person - extend FriendlyId - friendly_id :name, use: :slugged - end - ``` - - Check the [gem documentation](https://github.com/norman/friendly_id) for more - information about its usage. - - * - Use `find_each` to iterate over a collection of AR objects. Looping through a - collection of records from the database (using the `all` 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. - [[link](#find-each)] - - - ```ruby - # bad - Person.all.each do |person| - person.do_awesome_stuff - end - - Person.where('age > 21').each do |person| - person.party_all_night! - end - - # good - Person.find_each do |person| - person.do_awesome_stuff - end - - Person.where('age > 21').find_each do |person| - person.party_all_night! - end - ``` - - * - Since [Rails creates callbacks for dependent - associations](https://github.com/rails/rails/issues/3458), always call - `before_destroy` callbacks that perform validation with `prepend: true`. - [[link](#before_destroy)] - - ```ruby - # bad (roles will be deleted automatically even if super_admin? is true) - has_many :roles, dependent: :destroy - - before_destroy :ensure_deletable - - def ensure_deletable - raise "Cannot delete super admin." if super_admin? - end - - # good - has_many :roles, dependent: :destroy - - before_destroy :ensure_deletable, prepend: true - - def ensure_deletable - raise "Cannot delete super admin." if super_admin? - end - ``` - - * - Define the `dependent` option to the `has_many` and `has_one` associations. - [[link](#has_many-has_one-dependent-option)] - - ```ruby - # bad - class Post < ActiveRecord::Base - has_many :comments - end - - # good - class Post < ActiveRecord::Base - has_many :comments, dependent: :destroy - end - ``` - - * - When persisting AR objects always use the exception raising bang! method or handle the method return value. - This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. - [[link](#save-bang)] - - ```ruby - # bad - user.create(name: 'Bruce') - - # bad - user.save - - # good - user.create!(name: 'Bruce') - # or - bruce = user.create(name: 'Bruce') - if bruce.persisted? - ... - else - ... - end - - # good - user.save! - # or - if user.save - ... - else - ... - end - ``` - -### ActiveRecord Queries - - * - Avoid string interpolation in - queries, as it will make your code susceptible to SQL injection - attacks. - [[link](#avoid-interpolation)] - - ```ruby - # bad - param will be interpolated unescaped - Client.where("orders_count = #{params[:orders]}") - - # good - param will be properly escaped - Client.where('orders_count = ?', params[:orders]) - ``` - - * - Consider using named placeholders instead of positional placeholders - when you have more than 1 placeholder in your query. - [[link](#named-placeholder)] - - ```ruby - # okish - Client.where( - 'created_at >= ? AND created_at <= ?', - params[:start_date], params[:end_date] - ) - - # good - Client.where( - 'created_at >= :start_date AND created_at <= :end_date', - start_date: params[:start_date], end_date: params[:end_date] - ) - ``` - - * - Favor the use of `find` over `where.take!`, `find_by!`, and `find_by_id!` - when you need to retrieve a single record by primary key id and raise - `ActiveRecord::RecordNotFound` when the record is not found. - [[link](#find)] - - ```ruby - # bad - User.where(id: id).take! - - # bad - User.find_by_id!(id) - - # bad - User.find_by!(id: id) - - # good - User.find(id) - ``` - - * - Favor the use of `find_by` over `where.take` and `find_by_attribute` - when you need to retrieve a single record by one or more attributes and return - `nil` when the record is not found. - [[link](#find_by)] - - ```ruby - # bad - User.where(email: email).take - User.where(first_name: 'Bruce', last_name: 'Wayne').take - - # bad - User.find_by_email(email) - # bad, deprecated in ActiveRecord 4.0, removed in 4.1+ - User.find_by_first_name_and_last_name('Bruce', 'Wayne') - - # good - User.find_by(email: email) - User.find_by(first_name: 'Bruce', last_name: 'Wayne') - ``` - - * - Favor the use of `where.not` over SQL. - [[link](#where-not)] - - ```ruby - # bad - User.where("id != ?", id) - - # good - User.where.not(id: id) - ``` - - * - Don't use the `id` 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. - [[link](#order-by-id)] - - ```ruby - # bad - scope :chronological, -> { order(id: :asc) } - - # good - scope :chronological, -> { order(created_at: :asc) } - ``` - - * - Favor the use of `ids` over `pluck(:id)`. - [[link](#ids)] - - ```Ruby - # bad - User.pluck(:id) - - # good - User.ids - ``` - - * - When specifying an explicit query in a method such as `find_by_sql`, use - heredocs with `squish`. 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). - [[link](#squished-heredocs)] - - ```ruby - 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`](https://api.rubyonrails.org/classes/String.html#method-i-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 acounts\n ON\n accounts.user_id = users.id - ``` - - * - When querying ActiveRecord collections, prefer `size` (selects between count/length behavior based on whether collection is already loaded) or `length` (always loads the whole collection and counts the array elements) over `count` (always does a database query for the count). - [[link](#size-over-count-or-length)] - - ```ruby - # bad - User.count - - # good - User.all.size - - # good - if you really need to load all users into memory - User.all.length - ``` - -## Migrations - - * - Keep the `schema.rb` (or `structure.sql`) under version control. - [[link](#schema-version)] - - * - Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty - database. - [[link](#db-schema-load)] - - * - Enforce default values in the migrations themselves instead of in the - application layer. - [[link](#default-migration-values)] - - ```ruby - # bad - application enforced default value - class Product < ActiveRecord::Base - def amount - self[:amount] || 0 - end - end - - # good - database enforced - class AddDefaultAmountToProducts < ActiveRecord::Migration - def change - change_column_default :products, :amount, 0 - end - end - ``` - - 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. - - * - Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord - supports foreign key constraints natively. - [[link](#foreign-key-constraints)] - - * - When writing constructive migrations (adding tables or columns), - use the `change` method instead of `up` and `down` methods. - [[link](#change-vs-up-down)] - - ```ruby - # the old way - class AddNameToPeople < ActiveRecord::Migration - def up - add_column :people, :name, :string - end - - def down - remove_column :people, :name - end - end - - # the new preferred way - class AddNameToPeople < ActiveRecord::Migration - def change - add_column :people, :name, :string - end - end - ``` - - * - 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 - [[link](#define-model-class-migrations)] - - ```ruby - # db/migrate/.rb - # frozen_string_literal: true - - # bad - class ModifyDefaultStatusForProducts < ActiveRecord::Migration - def change - old_status = 'pending_manual_approval' - new_status = 'pending_approval' - - reversible do |dir| - dir.up do - Product.where(status: old_status).update_all(status: new_status) - change_column :products, :status, :string, default: new_status - end - - dir.down do - Product.where(status: new_status).update_all(status: old_status) - change_column :products, :status, :string, default: old_status - end - end - end - end - - # 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. - class MigrationProduct < ActiveRecord::Base - self.table_name = :products - end - - class ModifyDefaultStatusForProducts < ActiveRecord::Migration - def change - old_status = 'pending_manual_approval' - new_status = 'pending_approval' - - reversible do |dir| - dir.up do - MigrationProduct.where(status: old_status).update_all(status: new_status) - change_column :products, :status, :string, default: new_status - end - - dir.down do - MigrationProduct.where(status: new_status).update_all(status: old_status) - change_column :products, :status, :string, default: old_status - end - end - end - end - ``` - - * - Name your foreign keys explicitly instead of relying on Rails auto-generated - FK names. (https://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) - [[link](#meaningful-foreign-key-naming)] - - ```ruby - # bad - class AddFkArticlesToAuthors < ActiveRecord::Migration - def change - add_foreign_key :articles, :authors - end - end - - # good - class AddFkArticlesToAuthors < ActiveRecord::Migration - def change - add_foreign_key :articles, :authors, name: :articles_author_id_fk - end - end - ``` - - * - Don't use non-reversible migration commands in the `change` method. - Reversible migration commands are listed below. - [ActiveRecord::Migration::CommandRecorder](https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html) - [[link](#reversible-migration)] - - ```ruby - # bad - class DropUsers < ActiveRecord::Migration - def change - drop_table :users - end - end - - # good - class DropUsers < ActiveRecord::Migration - def up - drop_table :users - end - - def down - create_table :users do |t| - t.string :name - end - end - end - - # good - # In this case, block will be used by create_table in rollback - # https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table - class DropUsers < ActiveRecord::Migration - def change - drop_table :users do |t| - t.string :name - end - end - end - ``` - -## Views - - * - Never call the model layer directly from a view. - [[link](#no-direct-model-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. - [[link](#no-complex-view-formatting)] - - * - Mitigate code duplication by using partial templates and layouts. - [[link](#partials)] - -* - Avoid using instance variables in partials, pass a local variable to `render` 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. -[[link](#no-instance-variables-in-partials)] - -```erb - - -<%= render 'course_description' %> - -<%= @course.description %> - - - -<%= render 'course_description', course: @course %> - -<%= course.description %> -``` - -## Internationalization - - * - 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 the - `config/locales` directory. - [[link](#locale-texts)] - - * - When the labels of an ActiveRecord model need to be translated, use the - `activerecord` scope: - [[link](#translated-labels)] - - ``` - en: - activerecord: - models: - user: Member - attributes: - user: - name: 'Full name' - ``` - - Then `User.model_name.human` will return "Member" and - `User.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 ActiveRecord - attributes. Place the locale files for the models in a folder `locales/models` and the - texts used in the views in folder `locales/views`. - [[link](#organize-locale-files)] - - * When organization of the locale files is done with additional directories, - these directories must be described in the `application.rb` file in order - to be loaded. - - ```ruby - # config/application.rb - config.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 the `locales` directory. - [[link](#shared-localization)] - - * - Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` - and `I18n.l` instead of `I18n.localize`. - [[link](#short-i18n)] - - * - Use "lazy" lookup for the texts used in views. Let's say we have the following - structure: - [[link](#lazy-lookup)] - - ``` - en: - users: - show: - title: 'User details page' - ``` - - The value for `users.show.title` can be looked up in the template - `app/views/users/show.html.haml` like this: - - ```ruby - = t '.title' - ``` - - * - Use the dot-separated keys in the controllers and models instead of specifying - the `:scope` option. The dot-separated call is easier to read and trace the - hierarchy. - [[link](#dot-separated-keys)] - - ```ruby - # bad - I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] - - # good - I18n.t 'activerecord.errors.messages.record_invalid' - ``` - - * - More detailed information about the Rails I18n can be found in the [Rails - Guides](https://guides.rubyonrails.org/i18n.html) - [[link](#i18n-guides)] - -## Assets - -Use the [assets pipeline](https://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within -your application. - - * - Reserve `app/assets` for custom stylesheets, javascripts, or images. - [[link](#reserve-app-assets)] - - * - Use `lib/assets` for your own libraries that don’t really fit into the - scope of the application. - [[link](#lib-assets)] - - * - Third party code such as [jQuery](https://jquery.com/) or - [bootstrap](https://twitter.github.com/bootstrap/) should be placed in - `vendor/assets`. - [[link](#vendor-assets)] - - * - When possible, use gemified versions of assets (e.g. - [jquery-rails](https://github.com/rails/jquery-rails), - [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails), - [bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass), - [zurb-foundation](https://github.com/zurb/foundation)). - [[link](#gem-assets)] - -## Mailers - - * - Name the mailers `SomethingMailer`. Without the Mailer suffix it isn't - immediately apparent what's a mailer and which views are related to the - mailer. - [[link](#mailer-name)] - - * - Provide both HTML and plain-text view templates. - [[link](#html-plain-email)] - - * - Enable errors raised on failed mail delivery in your development environment. - The errors are disabled by default. - [[link](#enable-delivery-errors)] - - ```ruby - # config/environments/development.rb - - config.action_mailer.raise_delivery_errors = true - ``` - - * - Use a local SMTP server like - [Mailcatcher](https://github.com/sj26/mailcatcher) in the development - environment. - [[link](#local-smtp)] - - ```ruby - # config/environments/development.rb - - config.action_mailer.smtp_settings = { - address: 'localhost', - port: 1025, - # more settings - } - ``` - - * - Provide default settings for the host name. - [[link](#default-hostname)] - - ```ruby - # config/environments/development.rb - config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } - - # config/environments/production.rb - config.action_mailer.default_url_options = { host: 'your_site.com' } - - # in your mailer class - default_url_options[:host] = 'your_site.com' - ``` - - * - Format the from and to addresses properly. Use the following format: - [[link](#email-addresses)] - - ```ruby - # in your mailer class - default from: 'Your Name ' - ``` - - * - Make sure that the e-mail delivery method for your test environment is set to - `test`: - [[link](#delivery-method-test)] - - ```ruby - # config/environments/test.rb - - config.action_mailer.delivery_method = :test - ``` - - * - The delivery method for development and production should be `smtp`: - [[link](#delivery-method-smtp)] - - ```ruby - # config/environments/development.rb, config/environments/production.rb - - config.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](https://github.com/fphilipe/premailer-rails) and - [roadie](https://github.com/Mange/roadie). - [[link](#inline-email-styles)] - - * - 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 - of [sidekiq](https://github.com/mperham/sidekiq) gem. - [[link](#background-email)] - - -## Active Support Core Extensions - - * - Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`. - [[link](#try-bang)] - -```ruby -# bad -obj.try! :fly - -# good -obj&.fly -``` - - * - Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. - [[link](#active_support_aliases)] - -```ruby -# 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 ActiveSupport extensions. - [[link](#active_support_extensions)] - -```ruby -# bad -(1..50).to_a.forty_two -1.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 ActiveSupport's `Array#inquiry`, and `String#inquiry`. - [[link](#inquiry)] - -```ruby -# bad - String#inquiry -ruby = 'two'.inquiry -ruby.two? - -# good -ruby = 'two' -ruby == 'two' - -# bad - Array#inquiry -pets = %w(cat dog).inquiry -pets.gopher? - -# good -pets = %w(cat dog) -pets.include? 'cat' -``` - -## Time - - * - Config your timezone accordingly in `application.rb`. - [[link](#tz-config)] - - ```ruby - 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 use `Time.parse`. - [[link](#time-parse)] - - ```ruby - # bad - Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone. - - # good - Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 - ``` - - * - Don't use [`String#to_time`](https://api.rubyonrails.org/classes/String.html#method-i-to_time) - [[link](#to-time)] - - ```ruby - # bad - assumes time string given is in the system's time zone. - '2015-03-02 19:05:37'.to_time - - # good - Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 - ``` - - * - Don't use `Time.now`. - [[link](#time-now)] - - ```ruby - # bad - Time.now # => Returns system time and ignores your configured time zone. - - # good - Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 - Time.current # Same thing but shorter. - ``` - -## Bundler - - * - Put gems used only for development or testing in the appropriate group in the - Gemfile. - [[link](#dev-test-gems)] - - * - 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. - [[link](#only-good-gems)] - - * - OS-specific gems will by default result in a constantly changing - `Gemfile.lock` for projects with multiple developers using different operating - systems. Add all OS X specific gems to a `darwin` group in the Gemfile, and - all Linux specific gems to a `linux` group: - [[link](#os-specific-gemfile-locks)] - - ```ruby - # Gemfile - group :darwin do - gem 'rb-fsevent' - gem 'growl' - end - - group :linux do - gem 'rb-inotify' - end - ``` - - To require the appropriate gems in the right environment, add the - following to `config/application.rb`: - - ```ruby - platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym - Bundler.require(platform) - ``` - - * - Do not remove the `Gemfile.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 a `bundle install`. - [[link](#gemfile-lock)] - -## Managing processes - - * - If your projects depends on various external processes use - [foreman](https://github.com/ddollar/foreman) to manage them. - [[link](#foreman)] - -# Further Reading - -There are a few excellent resources on Rails style, that you should consider if -you have time to spare: - - * [The Rails 5 Way](https://www.informit.com/store/rails-5-way-9780134657677) - * [Ruby on Rails Guides](https://guides.rubyonrails.org/) - * [Effective Testing with RSpec 3](https://pragprog.com/book/rspec3/effective-testing-with-rspec-3) - * [The Cucumber Book](https://pragprog.com/book/hwcuc/the-cucumber-book) - * [Everyday Rails Testing with RSpec](https://leanpub.com/everydayrailsrspec) - * [Rails 5 Test Prescriptions](https://pragprog.com/book/nrtest3/rails-5-test-prescriptions) - * [Better Specs for RSpec](http://betterspecs.org) - -# Contributing - -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 via -[Patreon](https://www.patreon.com/bbatsov). - -## How to Contribute? - -It's easy, just follow the [contribution guidelines](https://github.com/rubocop-hq/rails-style-guide/blob/master/CONTRIBUTING.md). - -# License - -![Creative Commons License](https://i.creativecommons.org/l/by/3.0/88x31.png) -This work is licensed under a [Creative Commons Attribution 3.0 Unported -License](https://creativecommons.org/licenses/by/3.0/deed.en_US) - -# Spread the Word - -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](https://twitter.com/bbatsov) From 3addd79a48aea4f2abb4b5e359d8c85be09bfe3f Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Fri, 7 Jun 2019 23:16:44 +0300 Subject: [PATCH 117/191] Mention rubocop-rails --- README.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.adoc b/README.adoc index f3116397..551a0aa0 100644 --- a/README.adoc +++ b/README.adoc @@ -51,6 +51,8 @@ I've tried to add the rationale behind the rules (if it's omitted I've assumed i 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. +https://github.com/rubocop-hq/rubocop[RuboCop], a static code analyzer (linter) and formatter, has a https://github.com/rubocop-hq/rubocop-rails[`rubocop-rails`] extension, based on this style guide. + == Configuration === Config initializers [[config-initializers]] From 88409227c2e5fa021cbdd5b96d23d2bdc4dad1c5 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Mon, 10 Jun 2019 01:50:14 +0300 Subject: [PATCH 118/191] Use proper Title Case in section titles Section titles were mostly auto-generated during AsciiDoc conversion, and some of them were off. But the main culprit that they were not using proper Title Case. --- README.adoc | 164 ++++++++++++++++++++++++++-------------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/README.adoc b/README.adoc index 551a0aa0..8148d7a2 100644 --- a/README.adoc +++ b/README.adoc @@ -55,16 +55,16 @@ https://github.com/rubocop-hq/rubocop[RuboCop], a static code analyzer (linter) == Configuration -=== Config initializers [[config-initializers]] +=== Config Initializers [[config-initializers]] Put custom initialization code in `config/initializers`. The code in initializers executes on application startup. -=== Gem initializers [[gem-initializers]] +=== Gem Initializers [[gem-initializers]] Keep initialization code for each gem in a separate file with the same name as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc. -=== Dev/test/prod configs [[dev-test-prod-configs]] +=== Dev/Test/Prod Configs [[dev-test-prod-configs]] Adjust accordingly the settings for development, test and production environment (in the corresponding files under `config/environments/`) @@ -78,15 +78,15 @@ Mark additional assets for precompilation (if any): config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js ) ---- -=== App config [[app-config]] +=== App Config [[app-config]] Keep configuration that's applicable to all environments in the `config/application.rb` file. -=== Staging like prod [[staging-like-prod]] +=== Staging Like Prod [[staging-like-prod]] Create an additional `staging` environment that closely resembles the `production` one. -=== Yaml config [[yaml-config]] +=== YAML Config [[yaml-config]] Keep any additional configuration in YAML files under the `config/` directory. @@ -99,7 +99,7 @@ Rails::Application.config_for(:yaml_file) == Routing -=== Member collection routes [[member-collection-routes]] +=== Member Collection Routes [[member-collection-routes]] When you need to add more actions to a RESTful resource (do you really need them at all?) use `member` and `collection` routes. @@ -124,7 +124,7 @@ resources :photos do end ---- -=== Many member collection routes [[many-member-collection-routes]] +=== Many Member Collection Routes [[many-member-collection-routes]] If you need to define multiple `member/collection` routes use the alternative block syntax. @@ -145,7 +145,7 @@ resources :photos do end ---- -=== Nested routes [[nested-routes]] +=== Nested Routes [[nested-routes]] Use nested routes to express better the relationship between ActiveRecord models. @@ -165,7 +165,7 @@ resources :posts do end ---- -=== Shallow routes [[shallow-routes]] +=== Shallow Routes [[shallow-routes]] If you need to nest routes more than 1 level deep then use the `shallow: true` option. This will save user from long URLs `posts/1/comments/5/versions/7/edit` and you from long URL helpers `edit_post_comment_version`. @@ -179,7 +179,7 @@ resources :posts, shallow: true do end ---- -=== Namespaced routes [[namespaced-routes]] +=== Namespaced Routes [[namespaced-routes]] Use namespaced routes to group related actions. @@ -192,7 +192,7 @@ namespace :admin do end ---- -=== No wild routes [[no-wild-routes]] +=== No Wild Routes [[no-wild-routes]] Never use the legacy wild controller route. This route will make all actions in every controller accessible via GET requests. @@ -203,25 +203,25 @@ This route will make all actions in every controller accessible via GET requests match ':controller(/:action(/:id(.:format)))' ---- -=== No match routes [[no-match-routes]] +=== No Match Routes [[no-match-routes]] Don't use `match` 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. == Controllers -=== Skinny controllers [[skinny-controllers]] +=== Skinny Controllers [[skinny-controllers]] 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). -=== One method [[one-method]] +=== One Method [[one-method]] Each controller action should (ideally) invoke only one method other than an initial find or new. -=== Shared instance variables [[shared-instance-variables]] +=== Shared Instance Variables [[shared-instance-variables]] Minimize the number of instance variables passed between a controller and a view. -=== Lexically scoped action filter [[lexically-scoped-action-filter]] +=== Lexically Scoped Action Filter [[lexically-scoped-action-filter]] 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. @@ -242,9 +242,9 @@ class UsersController < ApplicationController end ---- -== Controllers: rendering [[rendering]] +== Controllers: Rendering [[rendering]] -=== Inline rendering [[inline-rendering]] +=== Inline Rendering [[inline-rendering]] Prefer using a template over inline rendering. @@ -273,7 +273,7 @@ class ProductsController < ApplicationController end ---- -=== Plain text rendering [[plain-text-rendering]] +=== Plain Text Rendering [[plain-text-rendering]] Prefer `render plain:` over `render text:`. @@ -295,7 +295,7 @@ render plain: 'Ruby!' ... ---- -=== HTTP status code symbols [[http-status-code-symbols]] +=== HTTP Status Code Symbols [[http-status-code-symbols]] Prefer https://gist.github.com/mlanett/a31c340b132ddefa9cca[corresponding symbols] to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes. @@ -315,15 +315,15 @@ render status: :forbidden == Models -=== Model classes [[model-classes]] +=== Model Classes [[model-classes]] Introduce non-ActiveRecord model classes freely. -=== Meaningful model names [[meaningful-model-names]] +=== Meaningful Model Names [[meaningful-model-names]] Name the models with meaningful (but short) names without abbreviations. -=== ActiveAttr gem [[activeattr-gem]] +=== ActiveAttr Gem [[activeattr-gem]] If you need model objects that support ActiveRecord behavior (like validation) without the ActiveRecord database functionality use the https://github.com/cgriego/active_attr[ActiveAttr] gem. @@ -347,7 +347,7 @@ end For a more complete example refer to the http://railscasts.com/episodes/326-activeattr[RailsCast on the subject]. -=== Model business logic [[model-business-logic]] +=== Model Business Logic [[model-business-logic]] 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. @@ -355,7 +355,7 @@ Keep your models for business logic and data-persistence only. == Models: ActiveRecord [[activerecord]] -=== Keep ActiveRecord defaults [[keep-ar-defaults]] +=== Keep ActiveRecord Defaults [[keep-ar-defaults]] Avoid altering ActiveRecord defaults (table names, primary key, etc) unless you have a very good reason (like a database that's not under your control). @@ -368,7 +368,7 @@ class Transaction < ActiveRecord::Base end ---- -=== Macro style methods [[macro-style-methods]] +=== Macro Style Methods [[macro-style-methods]] Group macro-style methods (`has_many`, `validates`, etc) in the beginning of the class definition. @@ -411,7 +411,7 @@ class User < ActiveRecord::Base end ---- -=== Has many through [[has-many-through]] +=== `has_many :through` [[has-many-through]] Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many :through` allows additional attributes and validations on the join model. @@ -444,7 +444,7 @@ class Group < ActiveRecord::Base end ---- -=== Read attribute [[read-attribute]] +=== Read Attribute [[read-attribute]] Prefer `self[:attribute]` over `read_attribute(:attribute)`. @@ -461,7 +461,7 @@ def amount end ---- -=== Write attribute [[write-attribute]] +=== Write Attribute [[write-attribute]] Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. @@ -478,7 +478,7 @@ def amount end ---- -=== New style validations [[new-style-validations]] +=== New-style Validations [[new-style-validations]] Always use the http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/["new-style" validations]. @@ -492,7 +492,7 @@ validates_length_of :email, maximum: 100 validates :email, presence: true, length: { maximum: 100 } ---- -=== Single attribute validations [[single-attribute-validations]] +=== Single-attribute Validations [[single-attribute-validations]] To make validations easy to read, don't list multiple attributes per validation. @@ -507,7 +507,7 @@ validates :email, presence: true, length: { maximum: 100 } validates :password, presence: true ---- -=== Custom validator file [[custom-validator-file]] +=== Custom Validator File [[custom-validator-file]] When a custom validation is used more than once or the validation is some regular expression mapping, create a custom validator file. @@ -530,15 +530,15 @@ class Person end ---- -=== App validators [[app-validators]] +=== App Validators [[app-validators]] Keep custom validators under `app/validators`. -=== Custom validators gem [[custom-validators-gem]] +=== Custom Validators Gem [[custom-validators-gem]] Consider extracting custom validators to a shared gem if you're maintaining several related apps or the validators are generic enough. -=== Named scopes [[named-scopes]] +=== Named Scopes [[named-scopes]] Use named scopes freely. @@ -552,7 +552,7 @@ class User < ActiveRecord::Base end ---- -=== Named scope class [[named-scope-class]] +=== Named Scope Class [[named-scope-class]] 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 an `ActiveRecord::Relation` object. Arguably you can define even simpler scopes like this. @@ -566,7 +566,7 @@ class User < ActiveRecord::Base end ---- -=== Callbacks order [[callbacks-order]] +=== Callbacks Order [[callbacks-order]] Order callback declarations in the order, in which they will be executed. For reference, see https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks[Available Callbacks]. @@ -586,7 +586,7 @@ class Person end ---- -=== Beware skip model validations [[beware-skip-model-validations]] +=== Beware Skip Model Validations [[beware-skip-model-validations]] Beware of the behavior of the https://guides.rubyonrails.org/active_record_validations.html#skipping-validations[following] methods. They do not run the model validations and could easily corrupt the model state. @@ -609,13 +609,13 @@ Post.update_counters 5, comment_count: -1, action_count: 1 user.update_attributes(website: 'example.com') ---- -=== User friendly URLs [[user-friendly-urls]] +=== User-friendly URLs [[user-friendly-urls]] Use user-friendly URLs. Show some descriptive attribute of the model in the URL rather than its `id`. There is more than one way to achieve this. -==== Override the `to_param` method of the model +==== Override the `to_param` Method of the Model This method is used by Rails for constructing a URL to the object. The default implementation returns the `id` of the record as a String. @@ -633,7 +633,7 @@ end In order to convert this to a URL-friendly value, `parameterize` should be called on the string. The `id` of the object needs to be at the beginning so that it can be found by the `find` method of ActiveRecord. -==== `friendly_id` gem +==== `friendly_id` Gem It allows creation of human-readable URLs by using some descriptive attribute of the model instead of its `id`. @@ -699,7 +699,7 @@ def ensure_deletable end ---- -=== `has_many`/`has_one` dependent option [[has_many-has_one-dependent-option]] +=== `has_many`/`has_one` Dependent Option [[has_many-has_one-dependent-option]] Define the `dependent` option to the `has_many` and `has_one` associations. @@ -751,7 +751,7 @@ end == Models: ActiveRecord Queries [[activerecord-queries]] -=== Avoid interpolation [[avoid-interpolation]] +=== Avoid Interpolation [[avoid-interpolation]] Avoid string interpolation in queries, as it will make your code susceptible to SQL injection attacks. @@ -764,7 +764,7 @@ Client.where("orders_count = #{params[:orders]}") Client.where('orders_count = ?', params[:orders]) ---- -=== Named placeholder [[named-placeholder]] +=== Named Placeholder [[named-placeholder]] Consider using named placeholders instead of positional placeholders when you have more than 1 placeholder in your query. @@ -822,7 +822,7 @@ User.find_by(email: email) User.find_by(first_name: 'Bruce', last_name: 'Wayne') ---- -=== Where not [[where-not]] +=== Where Not [[where-not]] Favor the use of `where.not` over SQL. @@ -864,7 +864,7 @@ User.pluck(:id) User.ids ---- -=== Squished heredocs [[squished-heredocs]] +=== Squished Heredocs [[squished-heredocs]] When specifying an explicit query in a method such as `find_by_sql`, use heredocs with `squish`. 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). @@ -908,15 +908,15 @@ User.all.length == Migrations -=== Schema version [[schema-version]] +=== Schema Version [[schema-version]] Keep the `schema.rb` (or `structure.sql`) under version control. -=== Db schema load [[db-schema-load]] +=== DB Schema Load [[db-schema-load]] Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty database. -=== Default migration values [[default-migration-values]] +=== Default Migration Values [[default-migration-values]] Enforce default values in the migrations themselves instead of in the application layer. @@ -940,11 +940,11 @@ end 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. -=== Foreign key constraints [[foreign-key-constraints]] +=== Foreign Key Constraints [[foreign-key-constraints]] Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord supports foreign key constraints natively. -=== Change vs up down [[change-vs-up-down]] +=== Change vs Up/Down [[change-vs-up-down]] When writing constructive migrations (adding tables or columns), use the `change` method instead of `up` and `down` methods. @@ -969,7 +969,7 @@ class AddNameToPeople < ActiveRecord::Migration end ---- -=== Define model class migrations [[define-model-class-migrations]] +=== Define Model Class Migrations [[define-model-class-migrations]] 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. @@ -1027,7 +1027,7 @@ class ModifyDefaultStatusForProducts < ActiveRecord::Migration end ---- -=== Meaningful foreign key naming [[meaningful-foreign-key-naming]] +=== Meaningful Foreign Key Naming [[meaningful-foreign-key-naming]] Name your foreign keys explicitly instead of relying on Rails auto-generated FK names. (https://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) @@ -1048,7 +1048,7 @@ class AddFkArticlesToAuthors < ActiveRecord::Migration end ---- -=== Reversible migration [[reversible-migration]] +=== Reversible Migration [[reversible-migration]] Don't use non-reversible migration commands in the `change` method. Reversible migration commands are listed below. @@ -1090,11 +1090,11 @@ end == Views -=== No direct model view [[no-direct-model-view]] +=== No Direct Model View [[no-direct-model-view]] Never call the model layer directly from a view. -=== No complex view formatting [[no-complex-view-formatting]] +=== No Complex View Formatting [[no-complex-view-formatting]] 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. @@ -1103,7 +1103,7 @@ A view helper is useful for simple cases, but if it's more complex then consider Mitigate code duplication by using partial templates and layouts. -=== No instance variables in partials [[no-instance-variables-in-partials]] +=== No Instance Variables in Partials [[no-instance-variables-in-partials]] Avoid using instance variables in partials, pass a local variable to `render` instead. The partial may be used in a different controller or action, where the variable can have a different name or even be absent. @@ -1126,12 +1126,12 @@ In these cases, an undefined instance variable will not raise an exception where == Internationalization -=== Locale texts [[locale-texts]] +=== Locale Texts [[locale-texts]] 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 the `config/locales` directory. -=== Translated labels [[translated-labels]] +=== Translated Labels [[translated-labels]] When the labels of an ActiveRecord model need to be translated, use the `activerecord` scope: @@ -1148,7 +1148,7 @@ en: Then `User.model_name.human` will return "Member" and `User.human_attribute_name("name")` will return "Full name". These translations of the attributes will be used as labels in the views. -=== Organize locale files [[organize-locale-files]] +=== Organize Locale Files [[organize-locale-files]] Separate the texts used in the views from translations of ActiveRecord attributes. Place the locale files for the models in a folder `locales/models` and the texts used in the views in folder `locales/views`. @@ -1161,15 +1161,15 @@ When organization of the locale files is done with additional directories, these config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] ---- -=== Shared localization [[shared-localization]] +=== Shared Localization [[shared-localization]] Place the shared localization options, such as date or currency formats, in files under the root of the `locales` directory. -=== Short i18n [[short-i18n]] +=== Short I18n [[short-i18n]] Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` and `I18n.l` instead of `I18n.localize`. -=== Lazy lookup [[lazy-lookup]] +=== Lazy Lookup [[lazy-lookup]] Use "lazy" lookup for the texts used in views. Let's say we have the following structure: @@ -1187,7 +1187,7 @@ The value for `users.show.title` can be looked up in the template `app/views/use = t '.title' ---- -=== Dot-separated keys [[dot-separated-keys]] +=== Dot-separated Keys [[dot-separated-keys]] Use the dot-separated keys in the controllers and models instead of specifying the `:scope` option. The dot-separated call is easier to read and trace the hierarchy. @@ -1201,7 +1201,7 @@ I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] I18n.t 'activerecord.errors.messages.record_invalid' ---- -=== I18n guides [[i18n-guides]] +=== I18n Guides [[i18n-guides]] More detailed information about the Rails I18n can be found in the https://guides.rubyonrails.org/i18n.html[Rails Guides] @@ -1228,16 +1228,16 @@ When possible, use gemified versions of assets (e.g. https://github.com/rails/jq == Mailers -=== Mailer name [[mailer-name]] +=== Mailer Name [[mailer-name]] Name the mailers `SomethingMailer`. Without the Mailer suffix it isn't immediately apparent what's a mailer and which views are related to the mailer. -=== HTML plain email [[html-plain-email]] +=== HTML Plain Email [[html-plain-email]] Provide both HTML and plain-text view templates. -=== Enable delivery errors [[enable-delivery-errors]] +=== Enable Delivery Errors [[enable-delivery-errors]] Enable errors raised on failed mail delivery in your development environment. The errors are disabled by default. @@ -1264,7 +1264,7 @@ config.action_mailer.smtp_settings = { } ---- -=== Default hostname [[default-hostname]] +=== Default Hostname [[default-hostname]] Provide default settings for the host name. @@ -1280,7 +1280,7 @@ config.action_mailer.default_url_options = { host: 'your_site.com' } default_url_options[:host] = 'your_site.com' ---- -=== Email addresses [[email-addresses]] +=== Email Addresses [[email-addresses]] Format the from and to addresses properly. Use the following format: @@ -1291,7 +1291,7 @@ Use the following format: default from: 'Your Name ' ---- -=== Delivery method test [[delivery-method-test]] +=== Delivery Method Test [[delivery-method-test]] Make sure that the e-mail delivery method for your test environment is set to `test`: @@ -1302,7 +1302,7 @@ Make sure that the e-mail delivery method for your test environment is set to `t config.action_mailer.delivery_method = :test ---- -=== Delivery method SMTP [[delivery-method-smtp]] +=== Delivery Method SMTP [[delivery-method-smtp]] The delivery method for development and production should be `smtp`: @@ -1313,13 +1313,13 @@ The delivery method for development and production should be `smtp`: config.action_mailer.delivery_method = :smtp ---- -=== Inline email styles [[inline-email-styles]] +=== Inline Email Styles [[inline-email-styles]] 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: https://github.com/fphilipe/premailer-rails[premailer-rails] and https://github.com/Mange/roadie[roadie]. -=== Background email [[background-email]] +=== Background Email [[background-email]] 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. @@ -1340,7 +1340,7 @@ obj.try! :fly obj&.fly ---- -=== ActiveSupport aliases [[active_support_aliases]] +=== ActiveSupport Aliases [[active_support_aliases]] Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. @@ -1355,7 +1355,7 @@ Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. 'the day'.end_with? 'ay' ---- -=== ActiveSupport extensions [[active_support_extensions]] +=== ActiveSupport Extensions [[active_support_extensions]] Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. @@ -1397,7 +1397,7 @@ pets.include? 'cat' == Time -=== Time zone config [[tz-config]] +=== Time Zone Config [[tz-config]] Configure your timezone accordingly in `application.rb`. @@ -1408,7 +1408,7 @@ config.time_zone = 'Eastern European Time' config.active_record.default_timezone = :local ---- -=== Time parse [[time-parse]] +=== `Time.parse` [[time-parse]] Don't use `Time.parse`. @@ -1450,11 +1450,11 @@ Time.current # Same thing but shorter. == Bundler -=== Dev/test gems [[dev-test-gems]] +=== Dev/Test Gems [[dev-test-gems]] Put gems used only for development or testing in the appropriate group in the Gemfile. -=== Only good gems [[only-good-gems]] +=== Only Good Gems [[only-good-gems]] 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. @@ -1490,7 +1490,7 @@ Bundler.require(platform) Do not remove the `Gemfile.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 a `bundle install`. -== Managing processes +== Managing Processes === Foreman [[foreman]] From 3e7658b03c60cf510077985a9e347bebb985d339 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 10 Jun 2019 17:53:22 +0300 Subject: [PATCH 119/191] Add a CI job to deploy to GitHub pages --- .circleci/config.yml | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..a979ffca --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,53 @@ +version: 2 + +jobs: + docs-build: + docker: + - image: ruby:2.6 + steps: + - checkout + - run: + name: Install AsciiDoctor & Rouge + command: | + gem install asciidoctor + gem install rouge + - run: + name: Build Site + command: asciidoctor -a toc="left" -a toclevels=2 README.adoc -o _build/html/index.html + - persist_to_workspace: + root: _build + paths: html + docs-deploy: + docker: + - image: node:8.10.0 + steps: + - checkout + - attach_workspace: + at: _build + - run: + name: Disable jekyll builds + command: touch _build/html/.nojekyll + - run: + name: Install and configure dependencies + command: | + npm install -g --silent gh-pages@2.0.1 + git config user.email "ci-build@rails.rubystyle.guide" + git config user.name "ci-build" + - add_ssh_keys: + fingerprints: + - "e4:5b:5f:9b:26:ad:46:71:8d:30:b7:12:3c:0a:57:2d" + - run: + name: Deploy docs to gh-pages branch + command: gh-pages -add --dotfiles --message "[skip ci] Update site" --dist _build/html + +workflows: + version: 2 + build: + jobs: + - docs-build + - docs-deploy: + requires: + - docs-build + filters: + branches: + only: master From 890cd461d349b5c22e6cf1ebf7533917b10fb7aa Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 10 Jun 2019 22:02:36 +0300 Subject: [PATCH 120/191] Enable syntax-highlighting for the HTML version of the guide --- README.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/README.adoc b/README.adoc index 551a0aa0..a0cca604 100644 --- a/README.adoc +++ b/README.adoc @@ -8,6 +8,7 @@ ifndef::backend-pdf[] :toc-title: pass:[

Table of Contents

] endif::[] +:source-highlighter: rouge [quote] ____ From 2a8cb5420df794991144b01b5878e39072c7fae2 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 10 Jun 2019 22:12:54 +0300 Subject: [PATCH 121/191] Add a tip to install rouge --- README.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.adoc b/README.adoc index a0cca604..a60ff68d 100644 --- a/README.adoc +++ b/README.adoc @@ -33,6 +33,16 @@ asciidoctor-pdf -a allow-uri-read README.adoc asciidoctor ---- +[TIP] +==== +Install the `rouge` gem to get nice syntax highlighting in the generated document. + +[source,shell] +---- +gem install rouge +---- +==== + Translations of the guide are available in the following languages: * https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md[Chinese Simplified] From 8f6bfe05e12dd0130ad229c4cb96e632c6d0b3fa Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 10 Jun 2019 22:13:12 +0300 Subject: [PATCH 122/191] Convert a note into a proper admonition --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index a60ff68d..f7c112d5 100644 --- a/README.adoc +++ b/README.adoc @@ -62,7 +62,7 @@ I've tried to add the rationale behind the rules (if it's omitted I've assumed i 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. -https://github.com/rubocop-hq/rubocop[RuboCop], a static code analyzer (linter) and formatter, has a https://github.com/rubocop-hq/rubocop-rails[`rubocop-rails`] extension, based on this style guide. +TIP: https://github.com/rubocop-hq/rubocop[RuboCop], a static code analyzer (linter) and formatter, has a https://github.com/rubocop-hq/rubocop-rails[`rubocop-rails`] extension, based on this style guide. == Configuration From 444dc7c8f94a29e2cd04b0fd43ba9c071de9f31d Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Mon, 10 Jun 2019 22:14:17 +0300 Subject: [PATCH 123/191] Reorder a bit of text to match more close the Ruby style guide --- README.adoc | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.adoc b/README.adoc index f7c112d5..9ea1343c 100644 --- a/README.adoc +++ b/README.adoc @@ -20,7 +20,15 @@ ____ The goal of this guide is to present a set of best practices and style prescriptions for Ruby on Rails 4 development. It's a complementary guide to the already existing community-driven https://github.com/rubocop-hq/ruby-style-guide[Ruby coding style guide]. -Some of the advice here is applicable only to Rails 4.0+. +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 Rails 4.0+. You can generate a PDF copy of this guide using https://asciidoctor.org/docs/asciidoctor-pdf/[AsciiDoctor PDF], and an HTML copy https://asciidoctor.org/docs/convert-documents/#converting-a-document-to-html[with] https://asciidoctor.org/#installation[AsciiDoctor] using the following commands: @@ -54,14 +62,6 @@ Translations of the guide are available in the following languages: * https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md[Vietnamese] * https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md[Portuguese (pt-BR)] -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. - TIP: https://github.com/rubocop-hq/rubocop[RuboCop], a static code analyzer (linter) and formatter, has a https://github.com/rubocop-hq/rubocop-rails[`rubocop-rails`] extension, based on this style guide. == Configuration From 9caad4dab41bc66e748bc4d1de9a61f033d74efa Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 12 Jun 2019 11:50:07 +0300 Subject: [PATCH 124/191] Mention the proper version of the guide --- README.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.adoc b/README.adoc index 9ea1343c..82735af8 100644 --- a/README.adoc +++ b/README.adoc @@ -17,6 +17,10 @@ Role models are important. -- Officer Alex J. Murphy / RoboCop ____ +ifdef::env-github[] +TIP: You can find a beautiful version of this guide with much improved navigation at https://rails.rubystyle.guide. +endif::[] + The goal of this guide is to present a set of best practices and style prescriptions for Ruby on Rails 4 development. It's a complementary guide to the already existing community-driven https://github.com/rubocop-hq/ruby-style-guide[Ruby coding style guide]. From c15a9e712b6f70365516bbaa49ab36165ee61d72 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 12 Jun 2019 13:30:47 +0300 Subject: [PATCH 125/191] Fix the CI build Rouge 3.4.0 has a nasty bug that breaks the html generation. See https://github.com/asciidoctor/asciidoctor/issues/3336 for details. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a979ffca..b1944a3d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,7 +10,7 @@ jobs: name: Install AsciiDoctor & Rouge command: | gem install asciidoctor - gem install rouge + gem install rouge -v 3.3.0 - run: name: Build Site command: asciidoctor -a toc="left" -a toclevels=2 README.adoc -o _build/html/index.html From e3f9ad2c71fd7975d9f9fb8278e0492049018239 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Wed, 12 Jun 2019 14:58:41 +0300 Subject: [PATCH 126/191] Replace a mention of Better Specs with the RSpec Style Guide --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 82735af8..a2324e50 100644 --- a/README.adoc +++ b/README.adoc @@ -1521,7 +1521,7 @@ There are a few excellent resources on Rails style, that you should consider if * https://pragprog.com/book/hwcuc/the-cucumber-book[The Cucumber Book] * https://leanpub.com/everydayrailsrspec[Everyday Rails Testing with RSpec] * https://pragprog.com/book/nrtest3/rails-5-test-prescriptions[Rails 5 Test Prescriptions] -* http://betterspecs.org[Better Specs for RSpec] +* https://rspec.rubystyle.guide[RSpec Style Guide] == Contributing From 7ae35a67ef4817316537c5e296108523e4080767 Mon Sep 17 00:00:00 2001 From: Bozhidar Batsov Date: Thu, 13 Jun 2019 10:51:30 +0300 Subject: [PATCH 127/191] Add an Introduction heading --- README.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.adoc b/README.adoc index a2324e50..78e3b393 100644 --- a/README.adoc +++ b/README.adoc @@ -10,6 +10,8 @@ ifndef::backend-pdf[] endif::[] :source-highlighter: rouge +== Introduction + [quote] ____ Role models are important. From 6386f77df4704fafac08f6a2962fce8136d5f062 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Fri, 28 Jun 2019 13:09:51 +0900 Subject: [PATCH 128/191] Use proper names of Rails compornents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR uses proper names of Rails compornents. > The proper names of Rails components have a space in between the words, > like “Active Support”. `ActiveRecord` is a Ruby module, whereas Active Record is an ORM. https://github.com/rails/rails/blob/v6.0.0.rc1/guides/source/api_documentation_guidelines.md#wording --- README.adoc | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/README.adoc b/README.adoc index c794e328..c9080ffc 100644 --- a/README.adoc +++ b/README.adoc @@ -164,7 +164,7 @@ end === Nested Routes [[nested-routes]] -Use nested routes to express better the relationship between ActiveRecord models. +Use nested routes to express better the relationship between Active Record models. [source,ruby] ---- @@ -334,7 +334,7 @@ render status: :forbidden === Model Classes [[model-classes]] -Introduce non-ActiveRecord model classes freely. +Introduce non-Active Record model classes freely. === Meaningful Model Names [[meaningful-model-names]] @@ -342,7 +342,7 @@ Name the models with meaningful (but short) names without abbreviations. === ActiveAttr Gem [[activeattr-gem]] -If you need model objects that support ActiveRecord behavior (like validation) without the ActiveRecord database functionality use the https://github.com/cgriego/active_attr[ActiveAttr] gem. +If you need model objects that support Active Record behavior (like validation) without the Active Record database functionality use the https://github.com/cgriego/active_attr[ActiveAttr] gem. [source,ruby] ---- @@ -370,11 +370,11 @@ Unless they have some meaning in the business domain, don't put methods in your 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. -== Models: ActiveRecord [[activerecord]] +== Models: Active Record [[activerecord]] -=== Keep ActiveRecord Defaults [[keep-ar-defaults]] +=== Keep Active Record Defaults [[keep-ar-defaults]] -Avoid altering ActiveRecord defaults (table names, primary key, etc) unless you have a very good reason (like a database that's not under your control). +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). [source,ruby] ---- @@ -648,7 +648,7 @@ end ---- In order to convert this to a URL-friendly value, `parameterize` should be called on the string. -The `id` of the object needs to be at the beginning so that it can be found by the `find` method of ActiveRecord. +The `id` of the object needs to be at the beginning so that it can be found by the `find` method of Active Record. ==== `friendly_id` Gem @@ -766,7 +766,7 @@ else end ---- -== Models: ActiveRecord Queries [[activerecord-queries]] +== Models: Active Record Queries [[activerecord-queries]] === Avoid Interpolation [[avoid-interpolation]] @@ -831,7 +831,7 @@ User.where(first_name: 'Bruce', last_name: 'Wayne').take # bad User.find_by_email(email) -# bad, deprecated in ActiveRecord 4.0, removed in 4.1+ +# bad, deprecated in Active Record 4.0, removed in 4.1+ User.find_by_first_name_and_last_name('Bruce', 'Wayne') # good @@ -909,7 +909,7 @@ SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n acount === `size` over `count` or `length` [[size-over-count-or-length]] -When querying ActiveRecord collections, prefer `size` (selects between count/length behavior based on whether collection is already loaded) or `length` (always loads the whole collection and counts the array elements) over `count` (always does a database query for the count). +When querying Active Record collections, prefer `size` (selects between count/length behavior based on whether collection is already loaded) or `length` (always loads the whole collection and counts the array elements) over `count` (always does a database query for the count). [source,ruby] ---- @@ -959,7 +959,7 @@ And you'll have to consider the fact that most non-trivial apps share a database === Foreign Key Constraints [[foreign-key-constraints]] -Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord supports foreign key constraints natively. +Enforce foreign-key constraints. As of Rails 4.2, Active Record supports foreign key constraints natively. === Change vs Up/Down [[change-vs-up-down]] @@ -1150,7 +1150,7 @@ These texts should be moved to the locale files in the `config/locales` director === Translated Labels [[translated-labels]] -When the labels of an ActiveRecord model need to be translated, use the `activerecord` scope: +When the labels of an Active Record model need to be translated, use the `activerecord` scope: ---- en: @@ -1167,7 +1167,7 @@ These translations of the attributes will be used as labels in the views. === Organize Locale Files [[organize-locale-files]] -Separate the texts used in the views from translations of ActiveRecord attributes. +Separate the texts used in the views from translations of Active Record attributes. Place the locale files for the models in a folder `locales/models` and the texts used in the views in folder `locales/views`. When organization of the locale files is done with additional directories, these directories must be described in the `application.rb` file in order to be loaded. @@ -1357,7 +1357,7 @@ obj.try! :fly obj&.fly ---- -=== ActiveSupport Aliases [[active_support_aliases]] +=== Active Support Aliases [[active_support_aliases]] Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. @@ -1372,9 +1372,9 @@ Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. 'the day'.end_with? 'ay' ---- -=== ActiveSupport Extensions [[active_support_extensions]] +=== Active Support Extensions [[active_support_extensions]] -Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. +Prefer Ruby's Standard Library over uncommon Active Support extensions. [source,ruby] ---- @@ -1391,7 +1391,7 @@ Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. === `inquiry` [[inquiry]] -Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, and `String#inquiry`. +Prefer Ruby's comparison operators over Active Support's `Array#inquiry`, and `String#inquiry`. [source,ruby] ---- From 9f5d24eacfaf423e95ae1adde42ab7f4dd497101 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 6 Jul 2019 23:52:52 +0900 Subject: [PATCH 129/191] Remove an incorrect comment for `find_by` example Dynamic finder methods (`DynamicMatchers`) has not been previously deprecated. AFAIK, next release Rails 6.0 has also no plans to deprecate it. https://github.com/rails/rails/blob/v6.0.0.rc1/activerecord/lib/active_record/dynamic_matchers.rb So, it seems that this was written incorrectly when I refer to the following. - https://github.com/rubocop-hq/rails-style-guide/pull/220 - https://github.com/rubocop-hq/rails-style-guide/issues/208 Therefore this PR removes the incorrect example comment for `find_by`. --- README.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/README.adoc b/README.adoc index c9080ffc..8807907e 100644 --- a/README.adoc +++ b/README.adoc @@ -831,7 +831,6 @@ User.where(first_name: 'Bruce', last_name: 'Wayne').take # bad User.find_by_email(email) -# bad, deprecated in Active Record 4.0, removed in 4.1+ User.find_by_first_name_and_last_name('Bruce', 'Wayne') # good From c1894f6ca53d4411874721a66e6f714ca420cffe Mon Sep 17 00:00:00 2001 From: Tejas Bubane Date: Mon, 8 Jul 2019 16:49:07 +0530 Subject: [PATCH 130/191] Add section for hash syntax of enums Closes https://github.com/rubocop-hq/rails-style-guide/issues/144 Ref: https://github.com/rubocop-hq/rubocop-rails/issues/78 --- README.adoc | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index c9080ffc..d43c87cb 100644 --- a/README.adoc +++ b/README.adoc @@ -385,6 +385,26 @@ class Transaction < ActiveRecord::Base end ---- +=== Enums [[enums]] + +Prefer using the hash syntax for `enum`. Array makes the database values implicit +& any insertion/removal/rearrangement of values in the middle will most probably +lead to broken code. + +[source,ruby] +---- +class Transaction < ActiveRecord::Base + # bad - implicit values - ordering matters + enum type: %i[credit debit] + + # good - explicit values - ordering does not matter + enum type: { + credit: 0, + debit: 1 + } +end +---- + === Macro Style Methods [[macro-style-methods]] Group macro-style methods (`has_many`, `validates`, etc) in the beginning of the class definition. @@ -403,7 +423,7 @@ class User < ActiveRecord::Base attr_accessible :login, :first_name, :last_name, :email, :password - # Rails 4+ enums after attr macros, prefer the hash syntax + # Rails 4+ enums after attr macros enum gender: { female: 0, male: 1 } # followed by association macros From 77e62930d60d3809c223d9f4416035966ce7098e Mon Sep 17 00:00:00 2001 From: dhnaranjo Date: Sat, 26 Oct 2019 17:05:25 -0500 Subject: [PATCH 131/191] Changed example enum --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 2c5bb1d4..ab8c9695 100644 --- a/README.adoc +++ b/README.adoc @@ -424,7 +424,7 @@ class User < ActiveRecord::Base attr_accessible :login, :first_name, :last_name, :email, :password # Rails 4+ enums after attr macros - enum gender: { female: 0, male: 1 } + enum role: { user: 0, moderator: 1, admin: 2 } # followed by association macros belongs_to :country From e3a2b5dd31837dc9618cf4fe735d33de18a7fe86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Huy=20H=C3=B9ng?= Date: Wed, 22 Jan 2020 15:18:30 +0700 Subject: [PATCH 132/191] Fix typo at Controller Rendering chapter --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index ab8c9695..2da34dab 100644 --- a/README.adoc +++ b/README.adoc @@ -282,7 +282,7 @@ end

<%= product.name %>

<%= product.price %>

-## app/controllers/foo_controller.rb +## app/controllers/products_controller.rb class ProductsController < ApplicationController def index render :index From 5b0e3fff4cb12b5d2874d8a9e8b4c22de5d61107 Mon Sep 17 00:00:00 2001 From: fatkodima Date: Sun, 28 Jun 2020 12:25:02 +0300 Subject: [PATCH 133/191] Add section about ActiveSupport's method --- README.adoc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.adoc b/README.adoc index 2da34dab..a664ceee 100644 --- a/README.adoc +++ b/README.adoc @@ -1431,6 +1431,23 @@ pets = %w(cat dog) pets.include? 'cat' ---- +=== `exclude?` [[exclude]] + +Prefer Active Support's `exclude?` over Ruby's negated `include?`. + +[source,ruby] +---- +# bad +!array.include?(2) +!hash.include?(:key) +!string.include?('substring') + +# good +array.exclude?(2) +hash.exclude?(:key) +string.exclude?('substring') +---- + == Time === Time Zone Config [[tz-config]] From 74a8994033a1a13bb643d804cffa95e7096d6767 Mon Sep 17 00:00:00 2001 From: Eugene Kenny Date: Sun, 28 Jun 2020 20:55:45 +0100 Subject: [PATCH 134/191] Add entries for pluck and pick --- README.adoc | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.adoc b/README.adoc index 2da34dab..3178265b 100644 --- a/README.adoc +++ b/README.adoc @@ -887,6 +887,38 @@ scope :chronological, -> { order(id: :asc) } scope :chronological, -> { order(created_at: :asc) } ---- +=== `pluck` + +Use https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-pluck[pluck] to select a single value from multiple records. + +[source,ruby] +---- +# bad +User.all.map(&:name) + +# bad +User.all.map { |user| user[:name] } + +# good +User.pluck(:name) +---- + +=== `pick` + +Use https://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-pick[pick] to select a single value from a single record. + +[source,ruby] +---- +# bad +User.pluck(:name).first + +# bad +User.first.name + +# good +User.pick(:name) +---- + === `ids` [[ids]] Favor the use of `ids` over `pluck(:id)`. From e419b440d058389b6d000acbff4d5181cb5d678c Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Tue, 30 Jun 2020 20:25:52 -0400 Subject: [PATCH 135/191] Remove unnecessary comma from callbacks guidance --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index a4b2e28e..a8421609 100644 --- a/README.adoc +++ b/README.adoc @@ -605,7 +605,7 @@ end === Callbacks Order [[callbacks-order]] -Order callback declarations in the order, in which they will be executed. +Order callback declarations in the order in which they will be executed. For reference, see https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks[Available Callbacks]. [source,Ruby] From 3c5cb5e83c93fc45654e4ad3f8db86120121b18b Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 6 Jul 2019 23:52:52 +0900 Subject: [PATCH 136/191] Remove an incorrect comment for `find_by` example Dynamic finder methods (`DynamicMatchers`) has not been previously deprecated. AFAIK, Rails 6.0 has also no plans to deprecate it. https://github.com/rails/rails/blob/v6.0.3.2/activerecord/lib/active_record/dynamic_matchers.rb So, it seems that this was written incorrectly when I refer to the following. - https://github.com/rubocop-hq/rails-style-guide/pull/220 - https://github.com/rubocop-hq/rails-style-guide/issues/208 Therefore this PR removes the incorrect example comment for `find_by`. --- README.adoc | 1 - 1 file changed, 1 deletion(-) diff --git a/README.adoc b/README.adoc index c9080ffc..8807907e 100644 --- a/README.adoc +++ b/README.adoc @@ -831,7 +831,6 @@ User.where(first_name: 'Bruce', last_name: 'Wayne').take # bad User.find_by_email(email) -# bad, deprecated in Active Record 4.0, removed in 4.1+ User.find_by_first_name_and_last_name('Bruce', 'Wayne') # good From 025e2275b8bc35b4cb52ea32ac5b30b97536fe07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Silva?= Date: Wed, 2 Sep 2020 15:51:42 -0400 Subject: [PATCH 137/191] In durations, Don't use `.since` without a parameter, prefer `.from_now`. --- README.adoc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.adoc b/README.adoc index a8421609..ca0b6445 100644 --- a/README.adoc +++ b/README.adoc @@ -1533,6 +1533,24 @@ Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 Time.current # Same thing but shorter. ---- +== Duration + +=== `.since` [[duration-since]] + +Don't use `.since` without a parameter, prefer `.from_now`. + +[source,ruby] +---- +# bad +5.hours.since # => It's not clear at first sight that `since` refers to "since now" + +# good +5.hours.from_now # when refering a future time from now, use from_now + +yesterday = 1.day.ago +tomorrow = 2.days.since(yesterday) # use since only when you have a referential Time in the past +---- + == Bundler === Dev/Test Gems [[dev-test-gems]] From 72fff7687379a1c6301a47ddb3f47c562c225ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Silva?= Date: Wed, 2 Sep 2020 17:20:32 -0400 Subject: [PATCH 138/191] add more cases of duration preferred usage --- README.adoc | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/README.adoc b/README.adoc index ca0b6445..83423e22 100644 --- a/README.adoc +++ b/README.adoc @@ -1535,20 +1535,39 @@ Time.current # Same thing but shorter. == Duration -=== `.since` [[duration-since]] - -Don't use `.since` without a parameter, prefer `.from_now`. +Don't use `.since`, `.after` without a parameter, prefer `.from_now`. +Dont't use `.until`, `.before`, without a parameter, prefer `ago`. +Dont't use `.ago' or `.from_now` with a parameter, prefer their respective aliases `.until` or `.before` for `ago` and `.since` or `.after` for `from_now` +Don't use hard coded negative numbers for the duration subject +As a general rule, make sure that reading the code as it was plain english makes total sense. [source,ruby] ---- # bad 5.hours.since # => It's not clear at first sight that `since` refers to "since now" +5.hours.after # => It's not clear at first sight that `after` refers to "after now" +5.hours.before # => It's not clear at first sight that `before` refers to "before now" +5.hours.until # => It's not clear at first sight that `until` refers to "until now" + +tomorrow = 2.days.from_now(yesterday) # yesterday would be 1.day.ago, reading this is confusing +today = 1.day.ago(tomorrow) + +-5.hours.from_now # reading this is confusing +-5.hours.ago # reading this is confusing +-5.hours.after(today) # good 5.hours.from_now # when refering a future time from now, use from_now +5.hours.ago # when refering a past time from now, use ago -yesterday = 1.day.ago tomorrow = 2.days.since(yesterday) # use since only when you have a referential Time in the past +tomorrow = 2.days.after(yesterday) # use after only when you have a referential Time in the past +today = 1.day.before(tomorrow) # use before only when you have a referential Time in the future +today = 1.day.until(tomorrow) # use until only when you have a referential Time in the future + +5.hours.ago # use the inverse method instead of the inverse number, -5.hours.from_now becomes 5.hours.ago +5.hours.ago # use the inverse method instead of the inverse number, -5.hours.ago becomes 5.hours.ago +5.hours.before(today) ---- == Bundler From ee59832172912d9b314fde0f4e45e0e75bd52499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Silva?= Date: Wed, 2 Sep 2020 17:26:30 -0400 Subject: [PATCH 139/191] add paragraphs --- README.adoc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.adoc b/README.adoc index 83423e22..fbf366b5 100644 --- a/README.adoc +++ b/README.adoc @@ -1536,9 +1536,13 @@ Time.current # Same thing but shorter. == Duration Don't use `.since`, `.after` without a parameter, prefer `.from_now`. + Dont't use `.until`, `.before`, without a parameter, prefer `ago`. + Dont't use `.ago' or `.from_now` with a parameter, prefer their respective aliases `.until` or `.before` for `ago` and `.since` or `.after` for `from_now` + Don't use hard coded negative numbers for the duration subject + As a general rule, make sure that reading the code as it was plain english makes total sense. [source,ruby] From 64b467c32e2e65319d3c3df40515e5fd5dcf8c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Silva?= Date: Thu, 3 Sep 2020 09:24:15 -0400 Subject: [PATCH 140/191] separate examples and rephrase statements --- README.adoc | 58 ++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/README.adoc b/README.adoc index fbf366b5..a0a827ad 100644 --- a/README.adoc +++ b/README.adoc @@ -1535,43 +1535,47 @@ Time.current # Same thing but shorter. == Duration -Don't use `.since`, `.after` without a parameter, prefer `.from_now`. +If used without a parameter, prefer `from_now` and `ago` instead of `since`, `after`, `until` or `before`. -Dont't use `.until`, `.before`, without a parameter, prefer `ago`. - -Dont't use `.ago' or `.from_now` with a parameter, prefer their respective aliases `.until` or `.before` for `ago` and `.since` or `.after` for `from_now` +[source,ruby] +---- +# bad - It's not clear at first sight that the qualifier refers to the current time (which is the default parameter) +5.hours.since +5.hours.after +5.hours.before +5.hours.until -Don't use hard coded negative numbers for the duration subject +# good +5.hours.from_now +5.hours.ago +---- -As a general rule, make sure that reading the code as it was plain english makes total sense. +If used with a parameter, prefer `since`, `after`, `until` or `before` instead of `from_now` and `ago`. [source,ruby] ---- -# bad -5.hours.since # => It's not clear at first sight that `since` refers to "since now" -5.hours.after # => It's not clear at first sight that `after` refers to "after now" -5.hours.before # => It's not clear at first sight that `before` refers to "before now" -5.hours.until # => It's not clear at first sight that `until` refers to "until now" - -tomorrow = 2.days.from_now(yesterday) # yesterday would be 1.day.ago, reading this is confusing -today = 1.day.ago(tomorrow) +# bad - It's confusing and misleading to read +2.days.from_now(yesterday) +2.days.ago(yesterday) - --5.hours.from_now # reading this is confusing --5.hours.ago # reading this is confusing --5.hours.after(today) # good -5.hours.from_now # when refering a future time from now, use from_now -5.hours.ago # when refering a past time from now, use ago +2.days.since(yesterday) +2.days.after(yesterday) +2.days.before(yesterday) +2.days.until(yesterday) +---- + +Avoid using literal negative numbers for the duration subject. Always prefer using a qualifier that allows using positive literal numbers. -tomorrow = 2.days.since(yesterday) # use since only when you have a referential Time in the past -tomorrow = 2.days.after(yesterday) # use after only when you have a referential Time in the past -today = 1.day.before(tomorrow) # use before only when you have a referential Time in the future -today = 1.day.until(tomorrow) # use until only when you have a referential Time in the future +[source,ruby] +---- +# bad - It's confusing and misleading to read +-5.hours.from_now +-5.hours.ago -5.hours.ago # use the inverse method instead of the inverse number, -5.hours.from_now becomes 5.hours.ago -5.hours.ago # use the inverse method instead of the inverse number, -5.hours.ago becomes 5.hours.ago -5.hours.before(today) +# good +5.hours.ago +5.hours.from_now ---- == Bundler From b2793c0bff353b08166b22f392c02bb748548b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjam=C3=ADn=20Silva?= Date: Thu, 3 Sep 2020 20:57:30 -0400 Subject: [PATCH 141/191] remove text that is not needed --- README.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index a0a827ad..3e377240 100644 --- a/README.adoc +++ b/README.adoc @@ -1539,7 +1539,7 @@ If used without a parameter, prefer `from_now` and `ago` instead of `since`, `af [source,ruby] ---- -# bad - It's not clear at first sight that the qualifier refers to the current time (which is the default parameter) +# bad - It's not clear that the qualifier refers to the current time (which is the default parameter) 5.hours.since 5.hours.after 5.hours.before @@ -1565,7 +1565,7 @@ If used with a parameter, prefer `since`, `after`, `until` or `before` instead o 2.days.until(yesterday) ---- -Avoid using literal negative numbers for the duration subject. Always prefer using a qualifier that allows using positive literal numbers. +Avoid using negative numbers for the duration subject. Always prefer using a qualifier that allows using positive literal numbers. [source,ruby] ---- From 035aeaf3e3cb1f0c3b7cc94e5e47e6d093b11280 Mon Sep 17 00:00:00 2001 From: Eugene Kenny Date: Mon, 21 Sep 2020 11:45:15 +0100 Subject: [PATCH 142/191] Extend query conditions guideline to cover equality Passing conditions as a hash is always preferable to using an SQL fragment, regardless of whether the conditions are negated. --- README.adoc | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index 3e377240..a952a601 100644 --- a/README.adoc +++ b/README.adoc @@ -858,12 +858,18 @@ User.find_by(email: email) User.find_by(first_name: 'Bruce', last_name: 'Wayne') ---- -=== Where Not [[where-not]] +=== Hash conditions [[where-not]] [[hash-conditions]] -Favor the use of `where.not` over SQL. +Favor passing conditions to `where` and `where.not` as a hash over using fragments of SQL. [source,ruby] ---- +# bad +User.where("name = ?", name) + +# good +User.where(name: name) + # bad User.where("id != ?", id) From ae7d303155e27adc4df3b3ce036161ae087b7338 Mon Sep 17 00:00:00 2001 From: Alan Savage Date: Thu, 1 Oct 2020 12:27:52 -0700 Subject: [PATCH 143/191] Add a link to the project repository --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index a952a601..e42619ed 100644 --- a/README.adoc +++ b/README.adoc @@ -1658,7 +1658,7 @@ You can also support the project (and RuboCop) with financial contributions via It's easy, just follow the contribution guidelines below: -* https://help.github.com/articles/fork-a-repo[Fork] the project on GitHub +* https://help.github.com/articles/fork-a-repo[Fork] the https://github.com/rubocop-hq/rails-style-guide[project] on GitHub * Make your feature addition or bug fix in a feature branch. * Include a http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[good description] of your changes * Push your feature branch to GitHub From 028c430dfcb783c823d050577a701a26040583f7 Mon Sep 17 00:00:00 2001 From: Ted Johansson Date: Fri, 27 Nov 2020 16:28:37 +0800 Subject: [PATCH 144/191] Recommend ActiveModel native modules over ActiveAttr gem --- README.adoc | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/README.adoc b/README.adoc index e42619ed..ae6e1eac 100644 --- a/README.adoc +++ b/README.adoc @@ -340,21 +340,16 @@ Introduce non-Active Record model classes freely. Name the models with meaningful (but short) names without abbreviations. -=== ActiveAttr Gem [[activeattr-gem]] +=== Non-ActiveRecord Models [[non-activerecord-models]] -If you need model objects that support Active Record behavior (like validation) without the Active Record database functionality use the https://github.com/cgriego/active_attr[ActiveAttr] gem. +If you need objects that support ActiveRecord-like behavior (like validations) without the database functionality, use `ActiveModel::Model`. [source,ruby] ---- class Message - include ActiveAttr::Model + include ActiveModel::Model - attribute :name - attribute :email - attribute :content - attribute :priority - - attr_accessible :name, :email, :content + attr_accessor :name, :email, :content, :priority validates :name, presence: true validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } @@ -362,7 +357,24 @@ class Message end ---- -For a more complete example refer to the http://railscasts.com/episodes/326-activeattr[RailsCast on the subject]. +Starting with Rails 6.1, you can also extend the attributes API from ActiveRecord using `ActiveModel::Attributes`. + +[source,ruby] +---- +class Message + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :name, :string + attribute :email, :string + attribute :content, :string + attribute :priority, :integer + + validates :name, presence: true + validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } + validates :content, length: { maximum: 500 } +end +---- === Model Business Logic [[model-business-logic]] From b329f736e7fff45f1866d96dcc9d978ae8d1fc49 Mon Sep 17 00:00:00 2001 From: Tejas Bubane Date: Wed, 25 Nov 2020 02:35:43 +0530 Subject: [PATCH 145/191] Add three-state boolean problem Issue: https://github.com/rubocop-hq/rubocop-rails/issues/337 Ref: https://thoughtbot.com/blog/avoid-the-threestate-boolean-problem --- README.adoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.adoc b/README.adoc index ae6e1eac..5a9bbeb8 100644 --- a/README.adoc +++ b/README.adoc @@ -1026,6 +1026,25 @@ end 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. +=== 3-state Boolean [[three-state-boolean]] + +With SQL databases, if a boolean column is not given a default value, it will have three possible values: `true`, `false` and `NULL`. +Boolean operators [work in unexpected ways](https://en.wikipedia.org/wiki/Three-valued_logic) with `NULL`. + +For example in SQL queries, `true AND NULL` is `NULL` (not false), `true AND NULL OR false` is `NULL` (not false). This can make SQL queries return unexpected results. + +To avoid such situations, boolean columns should always have a default value and a `NOT NULL` constraint. + +[source,ruby] +---- +# bad - boolean without a default value +add_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: false +add_column :users, :admin, :boolean, default: false, null: false +---- + === Foreign Key Constraints [[foreign-key-constraints]] Enforce foreign-key constraints. As of Rails 4.2, Active Record supports foreign key constraints natively. From c6a720277b9237e6aabd38e274803928e722a2be Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sun, 21 Feb 2021 00:49:03 +0300 Subject: [PATCH 146/191] Add a guideline to load config defaults Not loading the defaults will set you back on important Rails framework defaults, e.g.: - action_view.default_enforce_utf8 - action_controller.default_protect_from_forgery - active_record.belongs_to_required_by_default and is sometimes hard to notice. https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults Fixes #277 Co-authored-by: Andy Waite --- README.adoc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.adoc b/README.adoc index 5a9bbeb8..c390d35f 100644 --- a/README.adoc +++ b/README.adoc @@ -99,6 +99,16 @@ config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_ad Keep configuration that's applicable to all environments in the `config/application.rb` file. +=== Load Rails Config Defaults [[config-defaults]] + +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, the `config.load_defaults` setting should match your Rails version. + +[source,ruby] +---- +# good +config.load_defaults 6.1 +---- + === Staging Like Prod [[staging-like-prod]] Create an additional `staging` environment that closely resembles the `production` one. From e12f913aa6b63d80b5754aed20b0cad3275e90ed Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sun, 21 Feb 2021 04:30:49 +0300 Subject: [PATCH 147/191] Discourage using dedicated staging environment Fixes #240 See: https://12factor.net/dev-prod-parity Co-authored-by: Andy Waite --- README.adoc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index c390d35f..30340b04 100644 --- a/README.adoc +++ b/README.adoc @@ -111,7 +111,8 @@ config.load_defaults 6.1 === Staging Like Prod [[staging-like-prod]] -Create an additional `staging` environment that closely resembles the `production` one. +Avoid creating additional environment configurations than the defaults of `development`, `test` and `production`. +If you need a production-like environment such as staging, use environment variables for configuration options. === YAML Config [[yaml-config]] From 9a8abd5c30812ef78b850335f0c01793f5ceef76 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Mon, 22 Feb 2021 18:06:15 +0300 Subject: [PATCH 148/191] Add Custom Validation Methods naming guideline Fixes #223 --- README.adoc | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.adoc b/README.adoc index 30340b04..a09cdad2 100644 --- a/README.adoc +++ b/README.adoc @@ -552,6 +552,32 @@ validates_length_of :email, maximum: 100 validates :email, presence: true, length: { maximum: 100 } ---- +=== Custom Validation Methods + +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 + +[source,ruby] +---- +# good +validate :expiration_date_cannot_be_in_the_past +validate :discount_cannot_be_greater_than_total_value +validate :ensure_same_topic + +# also good - explicit prefix +validate :validate_birthday_in_past +validate :validate_sufficient_quantity +validate :must_have_owner_with_no_other_items +validate :must_have_shipping_units + +# bad +validate :birthday_in_past +validate :owner_has_no_other_items +---- + === Single-attribute Validations [[single-attribute-validations]] To make validations easy to read, don't list multiple attributes per validation. From d2a9a38a319a4cd4df8cc3c37565f9120e2dde53 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Tue, 23 Feb 2021 17:43:44 +0300 Subject: [PATCH 149/191] fixup! Make a good example even better --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index a09cdad2..ee3a9507 100644 --- a/README.adoc +++ b/README.adoc @@ -565,7 +565,7 @@ When naming custom validation methods, adhere to the simple rules: # good validate :expiration_date_cannot_be_in_the_past validate :discount_cannot_be_greater_than_total_value -validate :ensure_same_topic +validate :ensure_same_topic_is_chosen # also good - explicit prefix validate :validate_birthday_in_past From bd44fa3614383e022323fcda6d5b50d8761ba7e5 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sun, 9 May 2021 14:46:54 -0400 Subject: [PATCH 150/191] Use `email_address_with_name` --- README.adoc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.adoc b/README.adoc index ee3a9507..8bed5885 100644 --- a/README.adoc +++ b/README.adoc @@ -1433,6 +1433,14 @@ Use the following format: default from: 'Your Name ' ---- +If you're using Rails 6.1 or higher, you can use the `email_address_with_name` method: + +[source,ruby] +---- +# in your mailer class +default from: email_address_with_name('Your Name', 'info@your_site.com') +---- + === Delivery Method Test [[delivery-method-test]] Make sure that the e-mail delivery method for your test environment is set to `test`: From 400e071a780277fed070b86a1c255438d415ea6f Mon Sep 17 00:00:00 2001 From: Georgiy Bykov Date: Mon, 9 Aug 2021 02:00:01 +0300 Subject: [PATCH 151/191] Change params order for email_address_with_name method --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 8bed5885..b21c0141 100644 --- a/README.adoc +++ b/README.adoc @@ -1438,7 +1438,7 @@ If you're using Rails 6.1 or higher, you can use the `email_address_with_name` m [source,ruby] ---- # in your mailer class -default from: email_address_with_name('Your Name', 'info@your_site.com') +default from: email_address_with_name('info@your_site.com', 'Your Name') ---- === Delivery Method Test [[delivery-method-test]] From 137100eef2be82eaf2c944ff8db4793a2066f35d Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Tue, 5 Oct 2021 13:47:00 +0300 Subject: [PATCH 152/191] Move duration application to a sub-guideline --- README.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.adoc b/README.adoc index b21c0141..5af233f9 100644 --- a/README.adoc +++ b/README.adoc @@ -1617,6 +1617,8 @@ Time.current # Same thing but shorter. == Duration +=== Duration Application + If used without a parameter, prefer `from_now` and `ago` instead of `since`, `after`, `until` or `before`. [source,ruby] From fd4aaa80974340748a0321efd8e02b0a57775dd6 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Tue, 5 Oct 2021 13:50:27 +0300 Subject: [PATCH 153/191] Add Duration Arithmetic guideline fixes #282 --- README.adoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.adoc b/README.adoc index 5af233f9..13ac39a5 100644 --- a/README.adoc +++ b/README.adoc @@ -1662,6 +1662,21 @@ Avoid using negative numbers for the duration subject. Always prefer using a qua 5.hours.from_now ---- +=== Duration Arithmetic + +Use Duration methods instead of adding and subtracting with the current time. + +[source,ruby] +---- +# bad +Time.current - 1.minute +Time.zone.now + 2.days + +# good +1.minute.ago +2.days.from_now +---- + == Bundler === Dev/Test Gems [[dev-test-gems]] From 8feec0c69aeb1146d26b67c8533b7d57958cb804 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sun, 10 Oct 2021 16:09:22 +0300 Subject: [PATCH 154/191] Add examples for enforcing FKs fixes #287 https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference --- README.adoc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.adoc b/README.adoc index 13ac39a5..47b10dca 100644 --- a/README.adoc +++ b/README.adoc @@ -1086,6 +1086,23 @@ add_column :users, :admin, :boolean, default: false, null: false Enforce foreign-key constraints. As of Rails 4.2, Active Record supports foreign key constraints natively. +[source,ruby] +---- +# bad - does not add foreign keys +create_table :comment do |t| + t.references :article + t.belongs_to :user + t.integer :category_id +end + +# good +create_table :comment do |t| + t.references :article, foreign_key: true + t.belongs_to :user, foreign_key: true + t.references :category, foreign_key: { to_table: :comment_categories } +end +---- + === Change vs Up/Down [[change-vs-up-down]] When writing constructive migrations (adding tables or columns), use the `change` method instead of `up` and `down` methods. From 9bfad538a4c93ef98e21bf12a2c4b952d3fb1e4d Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 27 Nov 2021 00:17:38 +0900 Subject: [PATCH 155/191] Add spell checking GitHub Actions workflow Follow up to https://github.com/rubocop/rubocop/pull/10128. This PR adds spell checking GitHub Actions workflow. --- .github/workflows/spell_checking.yml | 33 ++++++++++++++++++++++++++++ README.adoc | 2 +- codespell.txt | 1 + 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/spell_checking.yml create mode 100644 codespell.txt diff --git a/.github/workflows/spell_checking.yml b/.github/workflows/spell_checking.yml new file mode 100644 index 00000000..55e8919b --- /dev/null +++ b/.github/workflows/spell_checking.yml @@ -0,0 +1,33 @@ +name: Spell Checking + +on: [pull_request] + +jobs: + codespell: + name: Check spelling with codespell + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.8] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install codespell + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Check spelling with codespell + run: codespell --ignore-words=codespell.txt || exit 1 + misspell: + name: Check spelling with misspell + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install + run: wget -O - -q https://git.io/misspell | sh -s -- -b . + - name: Misspell + run: ./misspell -error diff --git a/README.adoc b/README.adoc index 47b10dca..6000f301 100644 --- a/README.adoc +++ b/README.adoc @@ -1010,7 +1010,7 @@ SQL https://api.rubyonrails.org/classes/String.html#method-i-squish[`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 acounts\n ON\n accounts.user_id = users.id +SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n accounts\n ON\n accounts.user_id = users.id ---- === `size` over `count` or `length` [[size-over-count-or-length]] diff --git a/codespell.txt b/codespell.txt new file mode 100644 index 00000000..1cd3c8cd --- /dev/null +++ b/codespell.txt @@ -0,0 +1 @@ +rouge From 281ca95c41fff4020cf0961a5dc8a96e11d2e08a Mon Sep 17 00:00:00 2001 From: Paulo Henrique Cuchi Date: Wed, 5 Jan 2022 10:22:59 -0300 Subject: [PATCH 156/191] Fix code highlighting issues --- README.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.adoc b/README.adoc index 6000f301..0bd83eca 100644 --- a/README.adoc +++ b/README.adoc @@ -657,15 +657,15 @@ end Order callback declarations in the order in which they will be executed. For reference, see https://guides.rubyonrails.org/active_record_callbacks.html#available-callbacks[Available Callbacks]. -[source,Ruby] +[source,ruby] ---- -#bad +# bad class Person after_commit :after_commit_callback before_validation :before_validation_callback end -#good +# good class Person before_validation :before_validation_callback after_commit :after_commit_callback @@ -978,7 +978,7 @@ User.pick(:name) Favor the use of `ids` over `pluck(:id)`. -[source,Ruby] +[source,ruby] ---- # bad User.pluck(:id) From 158d178a62373734aa2ed7dea8216d1881f6b3cc Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sat, 5 Feb 2022 08:12:03 -0500 Subject: [PATCH 157/191] Fix Asciidoctor instructions --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 0bd83eca..d5264e46 100644 --- a/README.adoc +++ b/README.adoc @@ -44,7 +44,7 @@ You can generate a PDF copy of this guide using https://asciidoctor.org/docs/asc asciidoctor-pdf -a allow-uri-read README.adoc # Generates README.html -asciidoctor +asciidoctor README.adoc ---- [TIP] From ecd1be07b25503148c8554aa1b39ce86275abc7f Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sat, 5 Feb 2022 08:35:24 -0500 Subject: [PATCH 158/191] Use consistent language --- README.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.adoc b/README.adoc index 0bd83eca..d3252600 100644 --- a/README.adoc +++ b/README.adoc @@ -871,7 +871,7 @@ Client.where( === `find` [[find]] -Favor the use of `find` over `where.take!`, `find_by!`, and `find_by_id!` when you need to retrieve a single record by primary key id and raise `ActiveRecord::RecordNotFound` when the record is not found. +Prefer `find` over `where.take!`, `find_by!`, and `find_by_id!` when you need to retrieve a single record by primary key id and raise `ActiveRecord::RecordNotFound` when the record is not found. [source,ruby] ---- @@ -890,7 +890,7 @@ User.find(id) === `find_by` [[find_by]] -Favor the use of `find_by` over `where.take` and `find_by_attribute` when you need to retrieve a single record by one or more attributes and return `nil` when the record is not found. +Prefer `find_by` over `where.take` and `find_by_attribute` when you need to retrieve a single record by one or more attributes and return `nil` when the record is not found. [source,ruby] ---- @@ -909,7 +909,7 @@ User.find_by(first_name: 'Bruce', last_name: 'Wayne') === Hash conditions [[where-not]] [[hash-conditions]] -Favor passing conditions to `where` and `where.not` as a hash over using fragments of SQL. +Prefer passing conditions to `where` and `where.not` as a hash over using fragments of SQL. [source,ruby] ---- @@ -976,7 +976,7 @@ User.pick(:name) === `ids` [[ids]] -Favor the use of `ids` over `pluck(:id)`. +Prefer `ids` over `pluck(:id)`. [source,ruby] ---- From 6062dcc50362a836739994cc9f46525909b86bf3 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sat, 5 Feb 2022 08:41:56 -0500 Subject: [PATCH 159/191] Add guidance for controller testing --- README.adoc | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.adoc b/README.adoc index 0bd83eca..69e8f2f2 100644 --- a/README.adoc +++ b/README.adoc @@ -1736,6 +1736,23 @@ Bundler.require(platform) Do not remove the `Gemfile.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 a `bundle install`. +== Testing + +=== Integration Testing + +Prefer integration style controller tests over functional style controller tests, https://api.rubyonrails.org/v7.0.0/classes/ActionController/TestCase.html[as recommended in the Rails documentation]. + +[source,ruby] +---- +# bad +class MyControllerTest < ActionController::TestCase +end + +# good +class MyControllerTest < ActionDispatch::IntegrationTest +end +---- + == Managing Processes === Foreman [[foreman]] From be6184319ecad2bb1226e87926b8398a31c15b21 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sat, 5 Feb 2022 08:47:12 -0500 Subject: [PATCH 160/191] Add original author credit --- README.adoc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.adoc b/README.adoc index 0bd83eca..54f43bf3 100644 --- a/README.adoc +++ b/README.adoc @@ -12,6 +12,8 @@ endif::[] == Introduction +By https://github.com/bbatsov[Bozhidar Batsov] + [quote] ____ Role models are important. From fd418992bb3835f9aea343ae1b352360d155b660 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sat, 5 Feb 2022 11:39:50 -0500 Subject: [PATCH 161/191] Remove mentions of Rails 4 --- README.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index 0bd83eca..b7c46b27 100644 --- a/README.adoc +++ b/README.adoc @@ -23,7 +23,7 @@ ifdef::env-github[] TIP: You can find a beautiful version of this guide with much improved navigation at https://rails.rubystyle.guide. endif::[] -The goal of this guide is to present a set of best practices and style prescriptions for Ruby on Rails 4 development. +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-driven https://github.com/rubocop-hq/ruby-style-guide[Ruby 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. @@ -34,7 +34,7 @@ I've tried to add the rationale behind the rules (if it's omitted I've assumed i 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 Rails 4.0+. +NOTE: Some of the advice here is applicable only to recent versions of Rails. You can generate a PDF copy of this guide using https://asciidoctor.org/docs/asciidoctor-pdf/[AsciiDoctor PDF], and an HTML copy https://asciidoctor.org/docs/convert-documents/#converting-a-document-to-html[with] https://asciidoctor.org/#installation[AsciiDoctor] using the following commands: From 439cc94e3b3dd0a602fd991b9c2c4aab5826902a Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sat, 5 Feb 2022 20:55:25 -0500 Subject: [PATCH 162/191] Remove outdated platform advice --- README.adoc | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.adoc b/README.adoc index 0bd83eca..d61d4bc2 100644 --- a/README.adoc +++ b/README.adoc @@ -1705,32 +1705,6 @@ Put gems used only for development or testing in the appropriate group in the Ge 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. -=== OS-specific `Gemfile.lock` [[os-specific-gemfile-locks]] - -OS-specific gems will by default result in a constantly changing `Gemfile.lock` for projects with multiple developers using different operating systems. -Add all OS X specific gems to a `darwin` group in the Gemfile, and all Linux specific gems to a `linux` group: - -[source,ruby] ----- -# Gemfile -group :darwin do - gem 'rb-fsevent' - gem 'growl' -end - -group :linux do - gem 'rb-inotify' -end ----- - -To require the appropriate gems in the right environment, add the following to `config/application.rb`: - -[source,ruby] ----- -platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym -Bundler.require(platform) ----- - === `Gemfile.lock` [[gemfile-lock]] Do not remove the `Gemfile.lock` from version control. From a9babb6d2a22be99f4bb0357cd1f2b30f80b1359 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sat, 5 Feb 2022 21:00:57 -0500 Subject: [PATCH 163/191] Remove abandoned translations --- README.adoc | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.adoc b/README.adoc index 0bd83eca..eed884c6 100644 --- a/README.adoc +++ b/README.adoc @@ -59,14 +59,8 @@ gem install rouge Translations of the guide are available in the following languages: -* https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhCN.md[Chinese Simplified] -* https://github.com/JuanitoFatas/rails-style-guide/blob/master/README-zhTW.md[Chinese Traditional] * https://github.com/satour/rails-style-guide/blob/master/README-jaJA.md[Japanese] * https://github.com/arbox/rails-style-guide/blob/master/README-ruRU.md[Russian] -* https://github.com/tolgaavci/rails-style-guide/blob/master/README-trTR.md[Turkish] -* https://github.com/pureugong/rails-style-guide/blob/master/README-koKR.md[Korean] -* https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md[Vietnamese] -* https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md[Portuguese (pt-BR)] TIP: https://github.com/rubocop-hq/rubocop[RuboCop], a static code analyzer (linter) and formatter, has a https://github.com/rubocop-hq/rubocop-rails[`rubocop-rails`] extension, based on this style guide. From 0969390847bf176fb8721cd55a1b5ac500ecb167 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sun, 6 Feb 2022 12:25:51 +0300 Subject: [PATCH 164/191] Fix links --- README.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index 6c5dfc5a..33d02608 100644 --- a/README.adoc +++ b/README.adoc @@ -26,7 +26,7 @@ TIP: You can find a beautiful version of this guide with much improved navigatio endif::[] 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-driven https://github.com/rubocop-hq/ruby-style-guide[Ruby coding style guide]. +It's a complementary guide to the already existing community-driven https://github.com/rubocop/ruby-style-guide[Ruby 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. @@ -70,7 +70,7 @@ Translations of the guide are available in the following languages: * https://github.com/CQBinh/rails-style-guide/blob/master/README-viVN.md[Vietnamese] * https://github.com/abraaomiranda/rails-style-guide/blob/master/README-ptBR.md[Portuguese (pt-BR)] -TIP: https://github.com/rubocop-hq/rubocop[RuboCop], a static code analyzer (linter) and formatter, has a https://github.com/rubocop-hq/rubocop-rails[`rubocop-rails`] extension, based on this style guide. +TIP: https://github.com/rubocop/rubocop[RuboCop], a static code analyzer (linter) and formatter, has a https://github.com/rubocop/rubocop-rails[`rubocop-rails`] extension, based on this style guide. == Configuration @@ -1787,7 +1787,7 @@ You can also support the project (and RuboCop) with financial contributions via It's easy, just follow the contribution guidelines below: -* https://help.github.com/articles/fork-a-repo[Fork] the https://github.com/rubocop-hq/rails-style-guide[project] on GitHub +* https://help.github.com/articles/fork-a-repo[Fork] the https://github.com/rubocop/rails-style-guide[project] on GitHub * Make your feature addition or bug fix in a feature branch. * Include a http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[good description] of your changes * Push your feature branch to GitHub From 43b7590701e746a6f9bfed3dee7bc8bb23f58e68 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Sun, 6 Feb 2022 07:55:48 -0500 Subject: [PATCH 165/191] Replace references to `ActiveRecord::Base` with `ApplicationRecord` --- README.adoc | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/README.adoc b/README.adoc index 0bd83eca..03018dfa 100644 --- a/README.adoc +++ b/README.adoc @@ -179,11 +179,11 @@ Use nested routes to express better the relationship between Active Record model [source,ruby] ---- -class Post < ActiveRecord::Base +class Post < ApplicationRecord has_many :comments end -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord belongs_to :post end @@ -402,7 +402,7 @@ Avoid altering Active Record defaults (table names, primary key, etc) unless you [source,ruby] ---- # bad - don't do this if you can modify the schema -class Transaction < ActiveRecord::Base +class Transaction < ApplicationRecord self.table_name = 'order' ... end @@ -416,7 +416,7 @@ lead to broken code. [source,ruby] ---- -class Transaction < ActiveRecord::Base +class Transaction < ApplicationRecord # bad - implicit values - ordering matters enum type: %i[credit debit] @@ -434,7 +434,7 @@ Group macro-style methods (`has_many`, `validates`, etc) in the beginning of the [source,ruby] ---- -class User < ActiveRecord::Base +class User < ApplicationRecord # keep the default scope first (if any) default_scope { where(active: true) } @@ -479,26 +479,26 @@ Using `has_many :through` allows additional attributes and validations on the jo [source,ruby] ---- # not so good - using has_and_belongs_to_many -class User < ActiveRecord::Base +class User < ApplicationRecord has_and_belongs_to_many :groups end -class Group < ActiveRecord::Base +class Group < ApplicationRecord has_and_belongs_to_many :users end # preferred way - using has_many :through -class User < ActiveRecord::Base +class User < ApplicationRecord has_many :memberships has_many :groups, through: :memberships end -class Membership < ActiveRecord::Base +class Membership < ApplicationRecord belongs_to :user belongs_to :group end -class Group < ActiveRecord::Base +class Group < ApplicationRecord has_many :memberships has_many :users, through: :memberships end @@ -630,7 +630,7 @@ Use named scopes freely. [source,ruby] ---- -class User < ActiveRecord::Base +class User < ApplicationRecord scope :active, -> { where(active: true) } scope :inactive, -> { where(active: false) } @@ -645,7 +645,7 @@ Arguably you can define even simpler scopes like this. [source,ruby] ---- -class User < ActiveRecord::Base +class User < ApplicationRecord def self.with_orders joins(:orders).select('distinct(users.id)') end @@ -792,12 +792,12 @@ Define the `dependent` option to the `has_many` and `has_one` associations. [source,ruby] ---- # bad -class Post < ActiveRecord::Base +class Post < ApplicationRecord has_many :comments end # good -class Post < ActiveRecord::Base +class Post < ApplicationRecord has_many :comments, dependent: :destroy end ---- @@ -1046,7 +1046,7 @@ Enforce default values in the migrations themselves instead of in the applicatio [source,ruby] ---- # bad - application enforced default value -class Product < ActiveRecord::Base +class Product < ApplicationRecord def amount self[:amount] || 0 end From c9c4a51a249a5aa7d987d0f242d8146407497de5 Mon Sep 17 00:00:00 2001 From: Andy Waite Date: Wed, 16 Mar 2022 16:59:55 -0400 Subject: [PATCH 166/191] Fix bad link syntax Three-valued_logic --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index b366be84..004ea3d0 100644 --- a/README.adoc +++ b/README.adoc @@ -1062,7 +1062,7 @@ And you'll have to consider the fact that most non-trivial apps share a database === 3-state Boolean [[three-state-boolean]] With SQL databases, if a boolean column is not given a default value, it will have three possible values: `true`, `false` and `NULL`. -Boolean operators [work in unexpected ways](https://en.wikipedia.org/wiki/Three-valued_logic) with `NULL`. +Boolean operators https://en.wikipedia.org/wiki/Three-valued_logic[work in unexpected ways] with `NULL`. For example in SQL queries, `true AND NULL` is `NULL` (not false), `true AND NULL OR false` is `NULL` (not false). This can make SQL queries return unexpected results. From e72a1fca3ee7cf9c5963402611d691c47c0bb5f5 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Thu, 14 Apr 2022 11:37:03 +0900 Subject: [PATCH 167/191] Use `actions/checkout` version 3 https://github.com/actions/checkout/releases/tag/v3.0.0 --- .github/workflows/spell_checking.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spell_checking.yml b/.github/workflows/spell_checking.yml index 55e8919b..0a557a9e 100644 --- a/.github/workflows/spell_checking.yml +++ b/.github/workflows/spell_checking.yml @@ -10,7 +10,7 @@ jobs: matrix: python-version: [3.8] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: @@ -26,7 +26,7 @@ jobs: name: Check spelling with misspell runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install run: wget -O - -q https://git.io/misspell | sh -s -- -b . - name: Misspell From 2750e9dd7e4e3f816c0e20a8ced10a1ba2c405b2 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 16 Apr 2022 18:45:07 +0900 Subject: [PATCH 168/191] Add "Prefer `to_fs`" rule Follow up https://github.com/rails/rails/pull/44354. Also, the deprecated warning for `to_s(:db)` is `to_fs(:db)` instead of `to_formatted_s(:db)`. > DateTime#to_s(:db) is deprecated. Please use DateTime#to_fs(:db) instead. --- README.adoc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.adoc b/README.adoc index 004ea3d0..0f166106 100644 --- a/README.adoc +++ b/README.adoc @@ -1575,6 +1575,25 @@ hash.exclude?(:key) string.exclude?('substring') ---- +=== Prefer `to_fs` for Formatted Strings [[prefer-to-fs]] + +If you're using Rails 7.0 or higher, prefer `to_fs` over `to_formatted_s`. `to_formatted_s` is just too cumbersome for a method used that frequently. + +[source,ruby] +---- +# bad +time.to_formatted_s(:db) +date.to_formatted_s(:db) +datetime.to_formatted_s(:db) +42.to_formatted_s(:human) + +# good +time.to_fs(:db) +date.to_fs(:db) +datetime.to_fs(:db) +42.to_fs(:human) +---- + == Time === Time Zone Config [[tz-config]] From 252fadcf79f8820d33ac17f4e957e797648ff68b Mon Sep 17 00:00:00 2001 From: ydah <13041216+ydah@users.noreply.github.com> Date: Wed, 27 Apr 2022 21:12:27 +0900 Subject: [PATCH 169/191] Update links that make use of deprecated git.io https://github.blog/changelog/2022-04-25-git-io-deprecation/ --- .github/workflows/spell_checking.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spell_checking.yml b/.github/workflows/spell_checking.yml index 0a557a9e..51b44c2c 100644 --- a/.github/workflows/spell_checking.yml +++ b/.github/workflows/spell_checking.yml @@ -28,6 +28,6 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install - run: wget -O - -q https://git.io/misspell | sh -s -- -b . + run: wget -O - -q https://raw.githubusercontent.com/client9/misspell/master/install-misspell.sh | sh -s -- -b . - name: Misspell run: ./misspell -error From 420937f35400469d3df4c8d70b2553f2e7502e6f Mon Sep 17 00:00:00 2001 From: Felipe Sateler Date: Wed, 5 Jan 2022 15:32:30 -0300 Subject: [PATCH 170/191] Prefer appending to setting `ignored_columns` Setting may overwrite previous assignments and that is almost always a mistake. See also https://github.com/rubocop/rubocop-rails/pull/514 --- README.adoc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.adoc b/README.adoc index 0f166106..cb113781 100644 --- a/README.adoc +++ b/README.adoc @@ -404,6 +404,22 @@ class Transaction < ApplicationRecord end ---- +=== Always append to `ignored_columns` [[append-ignored-columns]] + +Avoid setting `ignored_columns`. It may overwrite previous assignments and that is almost always a mistake. Prefer appending to the list instead. + +[source,ruby] +---- +class Transaction < ApplicationRecord + # bad - it may overwrite previous assignments + self.ignored_columns = %i[legacy] + + # good - the value is appended to the list + self.ignored_columns += %i[legacy] + ... +end +---- + === Enums [[enums]] Prefer using the hash syntax for `enum`. Array makes the database values implicit From adde07e033f6f1ed5e87faac7fa0e8cffe679f92 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sat, 25 Dec 2021 12:05:26 +0300 Subject: [PATCH 171/191] Clarify Lazy lookup guideline a bit "the texts" is quite vague, as it stands for: > strings and other locale specific bits (such as date or currency formats) I believe "entries" is a better term. Lazy lookup is also available in controllers, see https://github.com/rails/rails/blob/fee61e3abc878e4d4930a0be0accf8e4569009a8/actionpack/lib/abstract_controller/translation.rb#L11 --- README.adoc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index cb113781..1512c122 100644 --- a/README.adoc +++ b/README.adoc @@ -1342,7 +1342,7 @@ Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` and === Lazy Lookup [[lazy-lookup]] -Use "lazy" lookup for the texts used in views. Let's say we have the following structure: +Use "lazy" lookup for locale entries from views and controllers. Let's say we have the following structure: ---- en: @@ -1355,6 +1355,10 @@ The value for `users.show.title` can be looked up in the template `app/views/use [source,ruby] ---- +# bad += t 'users.show.title' + +# good = t '.title' ---- From 7b91c8f4762b7880f4ee484488a9533238b779c1 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Sat, 25 Dec 2021 12:13:18 +0300 Subject: [PATCH 172/191] Relax dot-separated locale keys guideline --- README.adoc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index 1512c122..e230b9e7 100644 --- a/README.adoc +++ b/README.adoc @@ -1364,8 +1364,8 @@ The value for `users.show.title` can be looked up in the template `app/views/use === Dot-separated Keys [[dot-separated-keys]] -Use the dot-separated keys in the controllers and models instead of specifying the `:scope` option. -The dot-separated call is easier to read and trace the hierarchy. +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. [source,ruby] ---- @@ -1373,7 +1373,14 @@ The dot-separated call is easier to read and trace the hierarchy. I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] # good +I18n.t :record_invalid, scope: 'activerecord.errors.messages' I18n.t 'activerecord.errors.messages.record_invalid' + +# bad +I18n.t :title, scope: :invitation + +# good +I18n.t 'title.invitation' ---- === I18n Guides [[i18n-guides]] From dde963d77040e6c4c4d6833557aa6ce8407d9555 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 28 May 2022 17:12:55 +0900 Subject: [PATCH 173/191] Add "Prefer using squiggly heredoc over `strip_heredoc`" rule Follow up https://github.com/rubocop/rubocop-rails/pull/706. This PR adds "Prefer using squiggly heredoc over `strip_heredoc`" rule. --- README.adoc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.adoc b/README.adoc index 0f166106..a300b9e8 100644 --- a/README.adoc +++ b/README.adoc @@ -1575,6 +1575,28 @@ hash.exclude?(:key) string.exclude?('substring') ---- +=== Prefer using squiggly heredoc over `strip_heredoc` [[prefer-squiggly-heredoc]] + +If you're using Ruby 2.3 or higher, prefer squiggly heredoc (`<<~`) over Active Support's `strip_heredoc`. + +[source,ruby] +---- +# bad +< Date: Tue, 14 Jun 2022 22:06:11 +0900 Subject: [PATCH 174/191] Add "Prefer `freeze_time` over `travel_to` with an argument of the current time" rule Since there are several ways to indicate the current time, we believe that `freeze_time`, which can be expressed as simply as possible, should be preferred. ```ruby # bad travel_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) # good freeze_time ``` --- README.adoc | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.adoc b/README.adoc index e0979fae..76725c80 100644 --- a/README.adoc +++ b/README.adoc @@ -1791,6 +1791,24 @@ class MyControllerTest < ActionDispatch::IntegrationTest end ---- +=== `freeze_time` [[freeze-time]] + +Prefer https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-freeze_time[ActiveSupport::Testing::TimeHelpers#freeze_time] over https://api.rubyonrails.org/classes/ActiveSupport/Testing/TimeHelpers.html#method-i-travel_to[ActiveSupport::Testing::TimeHelpers#travel_to] with an argument of the current time. + +[source,ruby] +---- +# bad +travel_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) + +# good +freeze_time +---- + == Managing Processes === Foreman [[foreman]] From e4214dd5d63fc6b90644e4f1593e61fda9b1b144 Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Mon, 2 May 2022 18:17:07 +0300 Subject: [PATCH 175/191] Recommend using `where` with ranges --- README.adoc | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.adoc b/README.adoc index 76725c80..bd6463e2 100644 --- a/README.adoc +++ b/README.adoc @@ -1041,6 +1041,33 @@ User.all.size User.all.length ---- +=== Where with Ranges [[where-ranges]] + +Use ranges instead of defining comparative conditions using a template for scalar values. + +[source,ruby] +---- +# bad +User.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) + +# good +User.where(created_at: 30.days.ago..7.days.ago) + +# bad +User.where("created_at > ?", 7.days.ago) + +# good +User.where(created_at: 7.days.ago..) + +# okish - there is no range syntax that would denote inclusion on one end and +# exclusion on another. +Customer.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. + == Migrations === Schema Version [[schema-version]] From 43a9832f8bec70617a244d8c9a4c0e4a9f68221f Mon Sep 17 00:00:00 2001 From: Phil Pirozhkov Date: Mon, 23 May 2022 21:45:13 +0300 Subject: [PATCH 176/191] Adjust Named Placeholder not to interfere with Where with Ranges --- README.adoc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.adoc b/README.adoc index bd6463e2..e32db27e 100644 --- a/README.adoc +++ b/README.adoc @@ -870,14 +870,14 @@ Consider using named placeholders instead of positional placeholders when you ha ---- # okish Client.where( - 'created_at >= ? AND created_at <= ?', - params[:start_date], params[:end_date] + 'orders_count >= ? AND country_code = ?', + params[:min_orders_count], params[:country_code] ) # good Client.where( - 'created_at >= :start_date AND created_at <= :end_date', - start_date: params[:start_date], end_date: params[:end_date] + 'orders_count >= :min_orders_count AND country_code = :country_code', + min_orders_count: params[:min_orders_count], country_code: params[:country_code] ) ---- From eefb72a7037a81970385c1cfeb384f8bd29f823b Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 25 Jun 2022 16:48:33 +0900 Subject: [PATCH 177/191] Add new "Prefer `all_(day|week|month|quarter|year)` over range of date/time" rule Follow up https://github.com/rubocop/rubocop-rails/pull/730. This PR adds new "Prefer `all_(day|week|month|quarter|year)` over range of date/time" rule. ```ruby # bad date.beginning_of_day..date.end_of_day date.beginning_of_week..date.end_of_week date.beginning_of_month..date.end_of_month date.beginning_of_quarter..date.end_of_quarter date.beginning_of_year..date.end_of_year # good date.all_day date.all_week date.all_month date.all_quarter date.all_year ``` --- README.adoc | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.adoc b/README.adoc index e32db27e..ae308665 100644 --- a/README.adoc +++ b/README.adoc @@ -1723,6 +1723,28 @@ Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 Time.current # Same thing but shorter. ---- +=== Prefer `all_(day|week|month|quarter|year)` over range of date/time [[date-time-range]] + +Prefer `all_(day|week|month|quarter|year)` over `beginning_of_(day|week|month|quarter|year)..end_of_(day|week|month|quarter|year)` +to get the range of date/time. + +[source,ruby] +---- +# bad +date.beginning_of_day..date.end_of_day +date.beginning_of_week..date.end_of_week +date.beginning_of_month..date.end_of_month +date.beginning_of_quarter..date.end_of_quarter +date.beginning_of_year..date.end_of_year + +# good +date.all_day +date.all_week +date.all_month +date.all_quarter +date.all_year +---- + == Duration === Duration Application From d63886db31e5063628b69164deb762f5e8963a20 Mon Sep 17 00:00:00 2001 From: ydah <13041216+ydah@users.noreply.github.com> Date: Wed, 29 Jun 2022 16:30:13 +0900 Subject: [PATCH 178/191] Add "Finding missing relationship records" rule If you're using Rails 6.1 or higher, prefer `where.missing` over `left_joins` and `where` to find missing relationship records. It can be expressed in a more simplified. ```ruby # bad Post.left_joins(:author).where(authors: { id: nil }) # good Post.where.missing(:author) ``` --- README.adoc | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.adoc b/README.adoc index e32db27e..77781058 100644 --- a/README.adoc +++ b/README.adoc @@ -938,6 +938,19 @@ User.where("id != ?", id) User.where.not(id: id) ---- +=== Finding missing relationship records [[finding-missing-relationship-records]] + +If you're using Rails 6.1 or higher, use https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-missing[where.missing] to find missing relationship records. + +[source,ruby] +---- +# bad +Post.left_joins(:author).where(authors: { id: nil }) + +# good +Post.where.missing(:author) +---- + === Order by `id` [[order-by-id]] Don't use the `id` column for ordering. From 4efd6de739e22927249c0434a836a3563ae90640 Mon Sep 17 00:00:00 2001 From: Justin Dell Date: Sat, 2 Jul 2022 09:32:52 -0500 Subject: [PATCH 179/191] Correct endless range example and provide some clarity Resolves #321 --- README.adoc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 7fc68d2c..e2bd4153 100644 --- a/README.adoc +++ b/README.adoc @@ -1069,11 +1069,17 @@ User.where("created_at > :start AND created_at < end", start: 30.days.ago, end: User.where(created_at: 30.days.ago..7.days.ago) # bad -User.where("created_at > ?", 7.days.ago) +User.where("created_at >= ?", 7.days.ago) # good User.where(created_at: 7.days.ago..) +# note - ranges are inclusive or exclusive of their ending, not beginning +User.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 inclusion on one end and # exclusion on another. Customer.where("purchases_count > :min AND purchases_count <= :max", min: 0, max: 5) From b7207771c41854d5a1fa348b62c6612a84b108bc Mon Sep 17 00:00:00 2001 From: Justin Dell Date: Tue, 5 Jul 2022 09:09:12 -0500 Subject: [PATCH 180/191] provide more clarity to Where with Ranges --- README.adoc | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.adoc b/README.adoc index e2bd4153..fa0062a9 100644 --- a/README.adoc +++ b/README.adoc @@ -1061,9 +1061,9 @@ Use ranges instead of defining comparative conditions using a template for scala [source,ruby] ---- # bad -User.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) +User.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) # good User.where(created_at: 30.days.ago..7.days.ago) @@ -1080,8 +1080,7 @@ 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 inclusion on one end and -# exclusion on another. +# okish - there is no range syntax that would denote exclusion at the beginning of the range Customer.where("purchases_count > :min AND purchases_count <= :max", min: 0, max: 5) ---- From 9f1386f54af978673d96725cfeedc4ede8a00aa1 Mon Sep 17 00:00:00 2001 From: Andy Waite <13400+andyw8@users.noreply.github.com> Date: Wed, 3 Aug 2022 09:09:48 -0400 Subject: [PATCH 181/191] Remove definite article from title Following up from https://github.com/rubocop/ruby-style-guide/pull/900 --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 7fc68d2c..9c6432c8 100644 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= The Rails Style Guide += Rails Style Guide :idprefix: :idseparator: - :sectanchors: From 316530a3747a4359616f526b6e3015495aad9663 Mon Sep 17 00:00:00 2001 From: ydah <13041216+ydah@users.noreply.github.com> Date: Thu, 1 Dec 2022 07:57:03 +0900 Subject: [PATCH 182/191] Added dependabot for GitHub Actions How about using dependabot in this way? When this works, a PullRequest is created as follows: - https://github.com/ruby/bigdecimal/pull/242 Same as the PR here: https://github.com/rubocop/rubocop/pull/11215 --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..b18fd293 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'weekly' From 547b4c968478c49db721e3e7af8b6be2f8e503fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 3 Dec 2022 17:09:49 +0000 Subject: [PATCH 183/191] Bump actions/setup-python from 2 to 4 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 4. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v4) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/spell_checking.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spell_checking.yml b/.github/workflows/spell_checking.yml index 51b44c2c..01c50270 100644 --- a/.github/workflows/spell_checking.yml +++ b/.github/workflows/spell_checking.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From ded30adcf1b0f3e22da533f476861c51e8ad77d6 Mon Sep 17 00:00:00 2001 From: Tejas Bubane Date: Sat, 20 Nov 2021 23:24:57 +0530 Subject: [PATCH 184/191] Add section for where.not with multiple attributes Issue:https://github.com/rubocop/rubocop-rails/issues/565 --- README.adoc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.adoc b/README.adoc index f97d6cb9..d9363e60 100644 --- a/README.adoc +++ b/README.adoc @@ -1086,6 +1086,21 @@ Customer.where("purchases_count > :min AND purchases_count <= :max", min: 0, max 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. +=== `where.not` with multiple attributes + +Avoid passing multiple attributes to `where.not`. Rails logic in this case has changed in Rails 6.1 and +will 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. + +[source, ruby] +---- +# bad +User.where.not(status: 'active', plan: 'basic') + +# good +User.where.not('status = ? AND plan = ?', 'active', 'basic') +---- + == Migrations === Schema Version [[schema-version]] From 21598458816e61d18105815115af47e00dbfa30e Mon Sep 17 00:00:00 2001 From: masato-bkn Date: Fri, 11 Aug 2023 17:01:09 +0900 Subject: [PATCH 185/191] Add section for redundant `all` Co-authored-by: Koichi ITO --- README.adoc | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.adoc b/README.adoc index d9363e60..9a71bea0 100644 --- a/README.adoc +++ b/README.adoc @@ -1101,6 +1101,26 @@ User.where.not(status: 'active', plan: 'basic') User.where.not('status = ? AND plan = ?', 'active', 'basic') ---- +=== Redundant `all` [[redundant-all]] + +Using `all` as a receiver for Active Record query methods is redundant. +The result won't change without `all`, so it can be removed. + +[source, ruby] +---- +# bad +User.all.find(id) +User.all.order(:created_at) +users.all.where(id: ids) +user.articles.all.order(:created_at) + +# good +User.find(id) +User.order(:created_at) +users.where(id: ids) +user.articles.order(:created_at) +---- + == Migrations === Schema Version [[schema-version]] From 6f56a4ac6492e4a7d1815a578745eaed718cfa78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Sep 2023 19:34:19 +0000 Subject: [PATCH 186/191] Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/spell_checking.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/spell_checking.yml b/.github/workflows/spell_checking.yml index 01c50270..0000d702 100644 --- a/.github/workflows/spell_checking.yml +++ b/.github/workflows/spell_checking.yml @@ -10,7 +10,7 @@ jobs: matrix: python-version: [3.8] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: @@ -26,7 +26,7 @@ jobs: name: Check spelling with misspell runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install run: wget -O - -q https://raw.githubusercontent.com/client9/misspell/master/install-misspell.sh | sh -s -- -b . - name: Misspell From d4b93aebfab94cb17b6d76fc883b7f14062b5728 Mon Sep 17 00:00:00 2001 From: masato-bkn Date: Thu, 19 Oct 2023 14:38:50 +0900 Subject: [PATCH 187/191] add `mange` to codespell.txt Co-authored-by: Koichi ITO --- codespell.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/codespell.txt b/codespell.txt index 1cd3c8cd..0fdf89c4 100644 --- a/codespell.txt +++ b/codespell.txt @@ -1 +1,2 @@ +mange rouge From 7068195f897b6525109ba89617d819ef64ccdd4d Mon Sep 17 00:00:00 2001 From: masato-bkn Date: Tue, 17 Oct 2023 19:01:31 +0900 Subject: [PATCH 188/191] Add note to redundant-all section about `all` receiver being an association --- README.adoc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index 9a71bea0..47652a5c 100644 --- a/README.adoc +++ b/README.adoc @@ -1103,8 +1103,7 @@ User.where.not('status = ? AND plan = ?', 'active', 'basic') === Redundant `all` [[redundant-all]] -Using `all` as a receiver for Active Record query methods is redundant. -The result won't change without `all`, so it can be removed. +Using `all` as a receiver is redundant. The result won't change without `all`, so it should be removed. [source, ruby] ---- @@ -1121,6 +1120,17 @@ users.where(id: ids) user.articles.order(:created_at) ---- +NOTE: When the receiver for `all` is an association, there are methods whose behavior changes by omitting `all`. + +The following methods behave differently without `all`: + +* `delete` - https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-delete[with all] / https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-delete[without all] +* `delete_all` - https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_all[with all] / https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-delete_all[without all] +* `destroy` - https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-destroy[with all] / https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-destroy[without all] +* `destroy_all` - https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-destroy_all[with all] / https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-destroy_all[without all] + +So, when considering removing `all` from the receiver of these methods, it is recommended to refer to the documentation to understand how the behavior changes. + == Migrations === Schema Version [[schema-version]] From b58d33e152038871bf31b4e1ec28c4f002f00856 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:40:04 +0000 Subject: [PATCH 189/191] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/spell_checking.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/spell_checking.yml b/.github/workflows/spell_checking.yml index 0000d702..4e328e80 100644 --- a/.github/workflows/spell_checking.yml +++ b/.github/workflows/spell_checking.yml @@ -12,7 +12,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 4430bf70b49df39e91eff7b9b05f13da4ae88093 Mon Sep 17 00:00:00 2001 From: Ariel Kirkwood Date: Mon, 21 Oct 2024 19:04:39 -0400 Subject: [PATCH 190/191] Un-break PragProg links It appears PragProg updated "book" to "titles" in their URL structure. --- README.adoc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.adoc b/README.adoc index 47652a5c..475bc8ac 100644 --- a/README.adoc +++ b/README.adoc @@ -1933,10 +1933,10 @@ There are a few excellent resources on Rails style, that you should consider if * https://www.informit.com/store/rails-5-way-9780134657677[The Rails 5 Way] * https://guides.rubyonrails.org/[Ruby on Rails Guides] -* https://pragprog.com/book/rspec3/effective-testing-with-rspec-3[Effective Testing with RSpec 3] -* https://pragprog.com/book/hwcuc/the-cucumber-book[The Cucumber Book] +* https://pragprog.com/titles/rspec3/effective-testing-with-rspec-3/[Effective Testing with RSpec 3] +* https://pragprog.com/titles/hwcuc/the-cucumber-book/[The Cucumber Book] * https://leanpub.com/everydayrailsrspec[Everyday Rails Testing with RSpec] -* https://pragprog.com/book/nrtest3/rails-5-test-prescriptions[Rails 5 Test Prescriptions] +* https://pragprog.com/titles/nrtest3/rails-5-test-prescriptions/[Rails 5 Test Prescriptions] * https://rspec.rubystyle.guide[RSpec Style Guide] == Contributing From 2cfd487a79eb2c58624746bc4af8f29078e6ec85 Mon Sep 17 00:00:00 2001 From: Ariel Kirkwood Date: Mon, 21 Oct 2024 19:08:40 -0400 Subject: [PATCH 191/191] Link to second edition of The Cucumber Book --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 475bc8ac..4f1fc579 100644 --- a/README.adoc +++ b/README.adoc @@ -1934,7 +1934,7 @@ There are a few excellent resources on Rails style, that you should consider if * https://www.informit.com/store/rails-5-way-9780134657677[The Rails 5 Way] * https://guides.rubyonrails.org/[Ruby on Rails Guides] * https://pragprog.com/titles/rspec3/effective-testing-with-rspec-3/[Effective Testing with RSpec 3] -* https://pragprog.com/titles/hwcuc/the-cucumber-book/[The Cucumber Book] +* https://pragprog.com/titles/hwcuc2/the-cucumber-book-second-edition/[The Cucumber Book] * https://leanpub.com/everydayrailsrspec[Everyday Rails Testing with RSpec] * https://pragprog.com/titles/nrtest3/rails-5-test-prescriptions/[Rails 5 Test Prescriptions] * https://rspec.rubystyle.guide[RSpec Style Guide]