├── .gitignore ├── javascript ├── EDGE-CASES.md └── README.md ├── git ├── demo-branch.jpg ├── hotfix-branch.png ├── GitWorkflowDiagram.png ├── commits.md ├── README.md └── README-es.md ├── python ├── images │ ├── drf-spectacular-1.png │ └── drf-spectacular-2.png └── cookiecutter-django.md ├── .github ├── pull_request_template.md └── ISSUE_TEMPLATE │ └── new-discussion.md ├── node ├── FAQ.md └── README.md ├── code-review ├── pr-template.md └── README.md ├── ruby ├── rspec │ ├── README.md │ ├── tutorial │ │ └── let-vs-let.md │ ├── tutorial.md │ └── style_guide.md └── rails.md ├── css.md ├── pair_programming.md ├── open-source ├── OSS_README_example.md ├── README.md ├── rootstrap_contributing_guide.md └── developing_gems.md ├── README.md ├── pull-request └── README.md ├── infrastructure └── Heroku.md ├── flutter └── README.md └── kotlin └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /javascript/EDGE-CASES.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /git/demo-branch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/tech-guides/HEAD/git/demo-branch.jpg -------------------------------------------------------------------------------- /git/hotfix-branch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/tech-guides/HEAD/git/hotfix-branch.png -------------------------------------------------------------------------------- /git/GitWorkflowDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/tech-guides/HEAD/git/GitWorkflowDiagram.png -------------------------------------------------------------------------------- /python/images/drf-spectacular-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/tech-guides/HEAD/python/images/drf-spectacular-1.png -------------------------------------------------------------------------------- /python/images/drf-spectacular-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rootstrap/tech-guides/HEAD/python/images/drf-spectacular-2.png -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | [Discussion Link](https://github.com/rootstrap/tech-guides/issues/NUMBER) 2 | 3 | **Description:** 4 | -------------------------------------------------------------------------------- /node/FAQ.md: -------------------------------------------------------------------------------- 1 | # Node.JS FAQ 2 | 3 | ## Table of Contents 4 | 5 | 1. [When should I use node?](#1-node-usage) 6 | 2. [What version of Node should I pick? Why?](#2-node-versions) 7 | 3. [Typed vs Not typed](#3-node-typing) 8 | 4. [Node's Event Loop](#4-event-loop) 9 | 10 |

11 | -------------------------------------------------------------------------------- /code-review/pr-template.md: -------------------------------------------------------------------------------- 1 | ## Name Of the PR 2 | 3 | #### Trello board reference: 4 | 5 | * [Trello Card #]() 6 | 7 | --- 8 | 9 | #### Description: 10 | 11 | * 12 | 13 | --- 14 | 15 | #### Reviewers: 16 | 17 | * 18 | 19 | --- 20 | 21 | #### Notes: 22 | 23 | * 24 | 25 | --- 26 | 27 | #### Tasks: 28 | 29 | 30 | --- 31 | 32 | #### Risk: 33 | 34 | * 35 | 36 | --- 37 | 38 | #### Preview: 39 | 40 | * N/A 41 | -------------------------------------------------------------------------------- /ruby/rspec/README.md: -------------------------------------------------------------------------------- 1 | # RSpec guides 2 | 3 | RSpec is the most used testing tool in Ruby that we use here at Rootstrap. 4 | You can check our guides: 5 | 6 | * Do you want to know how to write specs? Check [RSpec Tutorial/Guide](tutorial.md) 7 | 8 | * Maintaining specs could be hard when the amount of tests and developers that work/worked in the project increases, so we have to stick to a standard, check our [RSpec Style Guide](style_guide.md) 9 | -------------------------------------------------------------------------------- /javascript/README.md: -------------------------------------------------------------------------------- 1 | # Js Style Guide 2 | 3 | - Check well known guide [Javascript Airbnb](https://github.com/airbnb/javascript) 4 | 5 | ## Table of Contents 6 | 7 | 1. [General Concepts](#1-general-concepts-best-practices) 8 | 2. [Scopes](#2-scopes-practices) 9 | 3. [Functions](#3-functions-best-practices) 10 | 4. [Objects & Prototypes](#4-objects-prototypes-best-practices) 11 | 5. [Arrays & Dictionaries](#5-arrays-dictionaries-best-practices) 12 | 6. [Concurrency](#6-concurrency-best-practices) 13 | 7. [Advanced](#7-advanced-best-practices) 14 | 8. [Code Quality and Complexity Management](#8-quality-complexity) 15 | 16 |

17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New discussion 3 | about: Start a new discussion 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Affected Guide:** 11 | A link to the guide/s that will receive changes. 12 | 13 | **Describe your motive to discuss:** 14 | A clear and concise description of what is motivating you to start this discussion. Add examples if possible. 15 | 16 | **Describe the solution you propose** 17 | A clear and concise description of what you propose. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots here. 24 | -------------------------------------------------------------------------------- /git/commits.md: -------------------------------------------------------------------------------- 1 | Rules of a great Git commit message 2 | =========== 3 | 4 | - Give a useful message (self-descriptive), avoid messages like: "Fix PR or bug" 5 | - Limit the subject line to 50 characters 6 | - Capitalize the subject line 7 | - Do not end the subject line with a period 8 | - Use the imperative mood in the subject line, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed". 9 | - Use the body to explain what and why vs. how 10 | 11 | Good commit messages serve at least three important purposes: 12 | 13 | - To help the future maintainers, say five years into the future, to find out why a particular change was made to the code or why a specific feature was added. 14 | - To help us write a good release note. 15 | - To speed up the reviewing process. 16 | -------------------------------------------------------------------------------- /css.md: -------------------------------------------------------------------------------- 1 | # The CSS Style Guide 2 | 3 | We follow https://cssguidelin.es. In the css naming convention, we are aligned with the Hyphen Delimited convention instead of BEM-like one. 4 | For SASS, we follow https://sass-guidelin.es/ 5 | 6 | ## CSS Units 7 | 8 | Refer to this table to decide which unit to use in each case. 9 | 10 | | Unit | Use for | 11 | |:-----------|:-----------------------------------------------------------------| 12 | | rem | font-size, dimensions (margin, padding, height, width) | 13 | | % | width (responsive containers) | 14 | | px | small details (borders, corners, shadows, images), media queries | 15 | | *unitless* | line-height | 16 | 17 | More info: 18 | 19 | - https://gist.github.com/janogarcia/8c140ff9294a285830a0 20 | - https://gist.github.com/basham/2175a16ab7c60ce8e001 -------------------------------------------------------------------------------- /pair_programming.md: -------------------------------------------------------------------------------- 1 | # Pair Programming 2 | 3 | **Before:** 4 | 5 | - Agree Scope (Pairing for two hours? Until the ticket is complete? Just get past the bug?) 6 | - Agree Physical/Virtual Location ("Will we both be comfortable here?") 7 | - Agree Working Environment (Two keyboards? Text editor and other tools) 8 | - Agree Pairing Style (Time-based? Ping pong?) 9 | 10 | **During:** 11 | 12 | - Keep the chat going (Think aloud. Encourage/support) 13 | - Keep switching (Follow the pairing style) 14 | - Keep both involved ("Could we do this another way?") 15 | - Keep breaking ([Have a break, have a…](https://www.youtube.com/watch?v=fejBO1HZXVQ)) 16 | - Keep checking in ("Could we search for a guide separately?") 17 | 18 | **After:** 19 | 20 | - Ask for Feedback (What did we do well? What could we have done better? It’ll feel weird, do it anyway) 21 | 22 | [Guide](https://github.com/thoughtbot/guides/tree/master/working-together) by thoughtbot, inc. / [CC BY](https://creativecommons.org/licenses/by/3.0/) 23 | -------------------------------------------------------------------------------- /open-source/OSS_README_example.md: -------------------------------------------------------------------------------- 1 | # [lib_name] 2 | 3 | 4 | ![Build Status](https://github.com/rootstrap/[lib_name]/workflows/CI/badge.svg) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/[token]/maintainability)](https://codeclimate.com/github/rootstrap/[lib_name]/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/[token]/test_coverage)](https://codeclimate.com/github/rootstrap/[lib_name]/test_coverage) 7 | 8 | Description 9 | 10 | ## Prerequisites 11 | - Install something 12 | 13 | ## Installation 14 | - Run some command 15 | 16 | ## Usage 17 | - Do something 18 | 19 | ## Contributing 20 | Bug reports (please use Issues) and pull requests are welcome on GitHub at https://github.com/rootstrap/[lib_name]. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. 21 | 22 | ## License 23 | The library is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 24 | 25 | ## Credits 26 | **[lib_name]** is maintained by [Rootstrap](http://www.rootstrap.com) with the help of our [contributors](https://github.com/rootstrap/[lib_name]/contributors). 27 | 28 | [](http://www.rootstrap.com) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rootstrap Guides 2 | 3 | This repository contains several guidelines that document processes and standards followed by our entire organization. We hope they may serve as a recommendation to anyone interested in following our quality standards. 4 | Guidelines are based on our own experiences and other companies' best practices. 5 | The guidelines exploit Git's advantages with regards to collaborative work, encouraging all of our developers to get actively involved with them. 6 | 7 | ### General 8 | 9 | - [Code review](./code-review) 10 | - [Git Workflow](./git) 11 | - [Git commits](./git/commits.md) 12 | - [Open Source](./open-source/README.md) 13 | - [Pair Programming](./pair_programming.md) 14 | - [Opening a Pull Request](./pull-request/README.md) 15 | 16 | ### Languages 17 | 18 | - [Ruby](./ruby) 19 | - [Swift](https://rootstrap.github.io/swift/) 20 | - [Javascript](./javascript) 21 | - [CSS](./css.md) 22 | - [Python](./python) 23 | - [Kotlin](./kotlin) 24 | 25 | ### Frameworks/Libraries 26 | 27 | - [Ruby on Rails](./ruby/rails.md) 28 | - [Angular 1](https://github.com/johnpapa/angular-styleguide/blob/master/a1) 29 | - [Angular 2+](https://angular.io/guide/styleguide) 30 | - [React](https://www.notion.so/rootstrap/Rootstrap-React-Guidelines-3a7f9ada653e44a185a729b437970a7b?pvs=4) 31 | - [RSpec](./ruby/rspec/README.md) 32 | - [Django](./python/cookiecutter-django.md) 33 | - [Node.Js](./node/README.md) 34 | - [Flutter](./flutter/README.md) 35 | 36 | ### Hosting Platforms 37 | 38 | - [Heroku](./infrastructure/Heroku.md) 39 | 40 | If you disagree with a guideline, open an issue on the guides repo rather than 41 | debating it within the code review. In the meantime, apply the guideline. 42 | -------------------------------------------------------------------------------- /ruby/rspec/tutorial/let-vs-let.md: -------------------------------------------------------------------------------- 1 | # let vs let! 2 | 3 | Understanding the difference between `let` and `let!` is fundamental when starting to learn RSpec. 4 | 5 | Use `let` to define a memoized helper method. The value will be cached 6 | across multiple calls in the same example but not across examples. 7 | 8 | Note that `let` is lazy-evaluated. That means that the expression is not evaluated until the first time it is invoked from somewhere else. You can use `let!` to force the expression's invocation before each example. 9 | 10 | ### When should `let!` be used? 11 | - When there's a need to trigger an insertion into the database before running an example block. 12 | - An alternative to `let!` is invoking the variable from a `before` or `subject` block, or even from inside an example. This practice is discouraged. 13 | 14 | ### Example 15 | 16 | #### Bad 17 | ``` 18 | let!(:user) { create(:user) } 19 | 20 | let!(:membership) { create(:membership, user: user) } 21 | ``` 22 | 23 | #### Good 24 | ``` 25 | let(:user) { create(:user) } 26 | 27 | let!(:membership) { create(:membership, user: user) } 28 | ``` 29 | --- 30 | #### Bad 31 | ``` 32 | let!(:params) do 33 | { 34 | email: 'user@example.com', 35 | password: 'password' 36 | } 37 | end 38 | ``` 39 | 40 | #### Good 41 | ``` 42 | let(:params) do 43 | { 44 | email: 'user@example.com', 45 | password: 'password' 46 | } 47 | end 48 | ``` 49 | Take a look at `params`. The expression is defining in-memory attributes, so there's no need to use `!`. 50 | 51 | --- 52 | 53 | In conclusion, prefer the use of `let` unless there is a compelling reason to choose `let!`. 54 | ### Resources: 55 | - [Let and let!](https://relishapp.com/rspec/rspec-core/v/2-5/docs/helper-methods/let-and-let) 56 | -------------------------------------------------------------------------------- /open-source/README.md: -------------------------------------------------------------------------------- 1 | # Open Source Recommendations 2 | 3 | Rootstrap enjoys being a part of the open source community and considers that open source is good for everyone. By embracing open source, it enables collaboration for solving specific problems and helps in reducing bugs. 4 | 5 | This is a guide for publishing and maintaining open source projects, check the Github guidelines for Open Source: 6 | - [Getting started](https://opensource.guide/starting-a-project/) 7 | - [How To contribute](https://opensource.guide/how-to-contribute/) 8 | - [Best practices for maintainers](https://opensource.guide/best-practices/) 9 | - [Code of Conduct](https://opensource.guide/code-of-conduct/) 10 | 11 | **Make sure to have:** 12 | - License (default: [MIT](https://opensource.org/licenses/MIT)) 13 | - Code of Conduct (default: [Contributor Covenant](https://www.contributor-covenant.org/)) 14 | - Contributing: We follow the [Rootstrap Contributing Guide](./rootstrap_contributing_guide.md) 15 | - Continuous Integration (default: [Github Actions](https://github.com/features/actions)) 16 | - Code Quality tools (default: [CodeClimate](https://codeclimate.com)) 17 | - Readme with: Description, Prerequisites/Installation, Usage, Company credits and links to items above (template: [README example](./OSS_README_example.md)) 18 | - Follow [semver](https://semver.org/) for good versioning. Start with version `0.1` and once it has been successfully used in at least one project upgrade to `1.0`. Generate a new git tag when releasing a new version. 19 | - For libs use the `master` branch not the `develop` branch as we'd normally do with apps. 20 | - Add https://rootstrap.com as the repo website and remove unused tabs and sections 21 | - 22 | 23 | 24 | **The items listed below are nice to have for the repository but aren't mandatory:** 25 | - Add Changelog 26 | - Blocked main branch for force push 27 | 28 | For ruby specific details check [developing Gems](./developing_gems.md) 29 | -------------------------------------------------------------------------------- /open-source/rootstrap_contributing_guide.md: -------------------------------------------------------------------------------- 1 | # Rootstrap Contributing Guide 2 | 3 | You can contribute to our open source projects if you have an issue, found a bug or think there's some functionality required that would add value to the gem. To do so, please check if there's not already an [issue](https://github.com/rootstrap/{repository}/issues) for that, if you find there's not, create a new one with as much detail as possible. 4 | 5 | If you want to contribute with code as well, please follow the next steps: 6 | 7 | 1. Read, understand and agree to our [code of conduct](https://github.com/rootstrap/{repository}/blob/master/CODE_OF_CONDUCT.md) 8 | 2. [Fork the repo](https://help.github.com/articles/about-forks/) 9 | 3. Clone the project into your machine: 10 | `$ git clone git@github.com:rootstrap/{repository}.git` 11 | 4. Access the repo: 12 | `$ cd {repository}` 13 | 5. Create your feature/bugfix branch: 14 | `$ git checkout -b your_new_feature` 15 | or 16 | `$ git checkout -b fix/your_fix` in case of a bug fix 17 | (if your PR is to address an existing issue, it would be good to name the branch after the issue, for example: if you are trying to solve issue 182, then a good idea for the branch name would be `182_your_new_feature`) 18 | 6. Write tests for your changes (feature/bug) 19 | 7. Code your (feature/bugfix) 20 | 8. Run the code analysis tool by doing: 21 | `$ rake code_analysis` 22 | 9. Run the tests: 23 | `$ bundle exec rspec` 24 | All tests must pass. If all tests (both code analysis and rspec) do pass, then you are ready to go to the next step: 25 | 10. Commit your changes: 26 | `$ git commit -m 'Your feature or bugfix title'` 27 | 11. Push to the branch `$ git push origin your_new_feature` 28 | 12. Create a new [pull request](https://help.github.com/articles/creating-a-pull-request/) 29 | 30 | Some helpful guides that will help you know how we work: 31 | 1. [Code review](https://github.com/rootstrap/tech-guides/tree/master/code-review) 32 | 2. [GIT workflow](https://github.com/rootstrap/tech-guides/tree/master/git) 33 | 3. [Ruby style guide](https://github.com/rootstrap/tech-guides/tree/master/ruby) 34 | 4. [Rails style guide](https://github.com/rootstrap/tech-guides/blob/master/ruby/rails.md) 35 | 5. [RSpec style guide](https://github.com/rootstrap/tech-guides/blob/master/ruby/rspec/README.md) 36 | 37 | For more information or guides like the ones mentioned above, please check our [tech guides](https://github.com/rootstrap/tech-guides). Keep in mind that the more you know about these guides, the easier it will be for your code to get approved and merged. 38 | 39 | Note: You can push as many commits as you want when working on a pull request, we just ask that they are descriptive and tell a story. Do try to open a pull request with just one commit but if you think you need to divide what you did into more commits to convey what you are trying to do, go for it. 40 | 41 | 42 | Thank you very much for your time and for considering helping in our projects. 43 | -------------------------------------------------------------------------------- /pull-request/README.md: -------------------------------------------------------------------------------- 1 | # Opening a Pull Request(PR) 2 | 3 | Think in your PR as something you are selling to your fellow developers, you the author are the seller, and the reviewers are the customers. You want to be as effective as possible in demostrating the value of your code, and why it is needed. This guide will try to help you accomplish that. 4 | 5 | 1. Define scope (What are you tackling on this PR?): 6 | 7 | **User Stories/Features:** 8 | 9 | The maximum scope of a PR should be a complete User Story. 10 | However, sometimes a single User Story may result in too many changes in the code base making your PR too 11 | difficult to review and once reviewed, too difficult to address all the comments you received. So if your PR is going to be bigger than 400 lines (for most techs), or whatever is the maximum allowed in the tech you're working, 12 | a better approach is to break the User Story even further. 13 | 14 | For example, we get this User Story: "As a user, I want to be able log in to the app." 15 | This User Story can be implemented in different PRs to facilitate review, as follows: 16 | 17 | - "As a user, I want to be able to enter my email and password into a log in form and hit the submit button" 18 | - "As a user I should see errors if the email or password I entered are incorrect" 19 | - "As a user I should be logged in to the system if I entered correct email and password" 20 | 21 | 22 | **Bug fixes/Minor tasks:** 23 | 24 | It may sound like a good idea to mix together a lot of small bug fixes in a single PR, yet this will result on reviewers having a hard time identifying the topic of your PR or even, understanding what is it about. 25 | A good practice would be to either have one PR per bug fix or -in some cases- have a few bug fixes that are related with each other in a single PR. 26 | 27 | 28 | 2. Implement the needed code changes: 29 | If tackling a big/complex feature is considered good practice to discuss the approach with teammates and tech colleagues that will be reviewing your code later. 30 | 3. Run/test your code manually to make sure it works and all the scoped functionality works as expected without breaking pre-existent functionality. 31 | 4. Make sure your Unit Tests, Automated Tests, etc document all the changes. 32 | 5. Make sure that pre-existent tests still run correctly. 33 | 6. Use descriptive messages in your commits. 34 | 7. Add any tags you consider relevant to your PR. 35 | 8. Choose a PR title that describes the main intent of it. 36 | 9. Add a description enumerating: Issues fixed, user stories addressed and any relevant information for the reviewer. This might include screenshots and/or gifs showcasing the intended feature. 37 | 10. Update status of User Story and link it to the PR. 38 | 11. Request a review for at least two developers familiar to the technology or code-base. 39 | If there are reviewers assigned to your project make sure to request a review from them. 40 | -------------------------------------------------------------------------------- /open-source/developing_gems.md: -------------------------------------------------------------------------------- 1 | # Gems 2 | 3 | ## Creating Gems/Rails Engines 4 | 5 | When creating a plain Ruby Gem use the [Rootstrap's gem generator]([https://github.com/rootstrap/rsgem](https://github.com/rootstrap/rsgem)): 6 | 7 | ```sh 8 | 9 | $ gem install rsgem 10 | $ rsgem new [name] 11 | 12 | ``` 13 | 14 | Or use the Bundler command: 15 | 16 | ```sh 17 | 18 | $ bundle gem [name] 19 | 20 | ``` 21 | 22 | When creating a Rails Engine use the Rails command: 23 | 24 | ```sh 25 | 26 | $ rails plugin new [name] --mountable 27 | 28 | ``` 29 | 30 | 31 | 32 | ## Gem version 33 | 34 | Keep gem version in a constant and use it like this: 35 | 36 | ```ruby 37 | 38 | spec.version = [name]::VERSION 39 | 40 | ``` 41 | 42 | 43 | 44 | ## Dependencies versions 45 | 46 | Take into account to restrict not only the lower required version of dependencies but also the upper supported version so to not break apps if any of your dependencies changes their API. Normally you should fix just to the major version. 47 | 48 | 49 | 50 | ## Code quality tools 51 | 52 | Make sure to add: 53 | 54 | - `Rubocop` 55 | 56 | - `Reek` 57 | 58 | 59 | 60 | ## Gemfile/Gemfile.lock/gemspec 61 | 62 | - `Gemfile` should only have the `gemspec` directive. An exception is when you need to develop against a gem that hasn't yet been released, in this case you can declare that dependency in the Gemfile: 63 | 64 | ```ruby 65 | 66 | gem 'rack', github: 'rack/rack' 67 | 68 | ``` 69 | 70 | - `Gemfile.lock` should be gitignored when developing gems. 71 | 72 | - `gemspec` is the place to declare dependencies. 73 | 74 | 75 | 76 | ## Testing 77 | 78 | Make the default rake task to run every intended check (tests, code analysis). 79 | 80 | ```sh 81 | 82 | bundle exec rake 83 | 84 | ``` 85 | 86 | 87 | 88 | For testing against different versions of `gem` dependencies you should add a `gemfiles` folder and inside it declare each separate `Gemfile` which can be then used by the CI. 89 | 90 | ## Release a new Gem version 91 | 92 | To be able to publish or update your gem in RubyGems, you first need to create a release and tag for your version. 93 | You can follow this steps: 94 | 1. Create a new branch for release purpose. 95 | 1. Update your gem `VERSION`. 96 | 1. Make sure your gemspec `spec.version` uses this new version. 97 | 1. You can also open a PR if your changes include more than just the version number. 98 | 1. Once your branch is merged, create a tag. 99 | ```sh 100 | $ git tag v[x.x.x] # Create a tag for version 101 | $ git push origin v[x.x.x] # Push the tag to Github 102 | ``` 103 | 104 | ## Publishing in RubyGems 105 | 106 | First create an account on [RubyGems](https://rubygems.org/sign_up). 107 | Then follow the next steps: 108 | 109 | ```sh 110 | 111 | $ gem build [gem-name].gemspec # Build the gem 112 | $ gem push [gem-build].gem # Push the gem to RubyGems 113 | $ gem owner --add rootstrap [gem-name] # Add Rootstrap as an Owner 114 | 115 | ``` 116 | -------------------------------------------------------------------------------- /node/README.md: -------------------------------------------------------------------------------- 1 | # Node.JS Style Guide 2 | 3 | ## Table of Contents 4 | 5 | 1. [Project Scaffolding & Structure](#1-project-structure-practices) 6 | 2. [Error Handling](#2-error-handling-practices) 7 | 3. [Code Style Practices](#3-code-style-practices) 8 | 4. [Security Practices](#4-security-best-practices) 9 | 5. [Performance Practices](#5-performance-best-practices) 10 | 6. [Express Specifics](#6-express-specifics-best-practices) 11 | 7. [Code Quality and Complexity Management](#8-quality-complexity) 12 | 13 |

14 | 15 | # `4. Security Best Practices` 16 | 17 | ## 4.1. Use Helmet 18 | 19 | It protects your app from some well-known web vulnerabilities by setting HTTP headers appropriately. 20 | Helmet is actually just a collection of smaller middleware functions that set security-related HTTP response headers, like: 21 | 22 | - CSP (Content-Security-Policy) header to prevent cross-site scripting attacks. 23 | - hidePoweredBy removes the X-Powered-By header. 24 | - ieNoOpen sets X-Download-Options for IE8+. 25 | 26 | **Otherwise:** Your app will be vulnerable to some well known attacks. 27 | 28 | 🔗 **READ MORE:** https://expressjs.com/en/advanced/best-practice-security.html#use-helmet 29 | 30 |

31 | 32 | ## 4.2. How to decide which Node.js version? 33 | 34 | Don’t use deprecated or vulnerable versions of the framework you are using. 35 | A good way to go is to always use the last stable version. 36 | 37 | **Otherwise:** You could have security issues and no support working on them 38 | 39 | 🔗 **READ MORE (Express.js specific):** https://expressjs.com/en/advanced/best-practice-security.html#dont-use-deprecated-or-vulnerable-versions-of-express 40 | 41 |

42 | 43 | # `5. Performance Best Practices` 44 | 45 | ## 5.1. Use gzip compression 46 | 47 | Gzip compressing can greatly decrease the size of the response body and hence increase the speed of a web app. 48 | 49 | **Otherwise:** You could face long waiting times in large paylods/high-traffic web apps. 50 | 51 | **GTK:** For a high-traffic website in production, the best way to put compression in place is to implement it at a reverse proxy level (see Use a reverse proxy). In that case, you do not need to use compression middleware. 52 | 53 |

54 | 55 | ## 5.2. Do not use console.log 56 | 57 | Not for the obvious reasons, but because console.log is synchronous when the destination is a terminal or a file, so it is not suitable for production unless you pipe the output to another program. 58 | 59 | **GTK:** Use a more mature logger like Winston or Bunyan 60 | 61 | 🔗 **READ MORE:** https://strongloop.com/strongblog/compare-node-js-logging-winston-bunyan/ 62 | 63 |

64 | 65 | ## 5.3. Set NODE_ENV='production' for Production Environment (Express.js specific) 66 | 67 | Setting NODE_ENV to “production” makes Express: 68 | 69 | - Cache view templates. 70 | - Cache CSS files generated from CSS extensions. 71 | - Generate less verbose error messages. 72 | 73 | 🔗 **READ MORE:** https://www.dynatrace.com/news/blog/the-drastic-effects-of-omitting-node-env-in-your-express-js-applications/ 74 | 75 |

76 | -------------------------------------------------------------------------------- /code-review/README.md: -------------------------------------------------------------------------------- 1 | Code Review 2 | =========== 3 | 4 | A guide for reviewing code and having your code reviewed. 5 | 6 | Everyone 7 | ---- 8 | 9 | - Accept that many programming decisions are opinions. Discuss tradeoffs, which you prefer, and reach a resolution quickly. 10 | - Ask questions; don't make demands. ("What do you think about naming this `:user_id`?") 11 | - Ask for clarification. ("I didn't understand. Can you clarify?") 12 | - Avoid selective ownership of code. ("mine", "not mine", "yours") 13 | - Avoid using terms that could be seen as referring to personal traits. ("dumb", 14 | "stupid"). Assume everyone is attractive, intelligent, and well-meaning. 15 | - Be explicit. Remember people don't always understand your intentions online. 16 | - Be humble. ("I'm not sure - let's look it up.") 17 | - Don't use hyperbole. ("always", "never", "endlessly", "nothing") 18 | - Don't use sarcasm. 19 | - Keep it real. If emoji, animated gifs, or humor aren't you, don't force them. If they are, use them with aplomb. 20 | - Talk in person if there are too many "I didn't understand" or "Alternative solution:" comments. Post a follow-up comment summarizing offline discussion. 21 | 22 | Having Your Code Reviewed 23 | ---- 24 | 25 | - Add 2 reviewers to the project (in some explicit cases could be more), the reviewers should have the expertise needed, ideally one of them has business knowledge of the project. 26 | - Add a good description to the PR, it helps the reviewers to understand the code and the context of it. You can follow [Pull Request Format](pr-template.md) 27 | - Add screenshots with the PR description if there are visual changes, gifs or before/after images are also appreciated but not mandatory. 28 | - Use labels to identify the PR status (ask to an repo admin if there are a missing label), some of them are: On hold and Awaiting info. 29 | - Be grateful for the reviewer's suggestions. ("Good call. I'll make that change.") 30 | - Don't take it personally. The review is of the code, not you. 31 | - Explain why the code exists. ("It's like that because of these reasons. Would it be more clear if I rename this class/file/method/variable?") 32 | - Link to the code review from the ticket/story. ("Ready for review: https://github.com/organization/project/pull/1") 33 | - Push commits based on earlier rounds of feedback as isolated commits to the branch. Do not squash until the branch is ready to merge. Reviewers should be able to read individual updates based on their earlier feedback. 34 | - Seek to understand the reviewer's perspective. 35 | - One of the reviewers or you can merge once: 36 | - Continuous Integration (CircleCi, Github Actions, etc) tells you the test suite is green in the branch. 37 | - Two reviewers approved the PR 38 | 39 | Reviewing Code 40 | ---- 41 | 42 | Understand why the change is necessary (fixes a bug, improves the user 43 | experience, refactors the existing code). Then: 44 | 45 | - Communicate which ideas you feel strongly about and those you don't. 46 | - Identify ways to simplify the code while still solving the problem. 47 | - If discussions turn too philosophical or academic, move the discussion offline to a regular Friday afternoon technique discussion. In the meantime, let the author make the final decision on alternative implementations. 48 | - Offer alternative implementations, but assume the author already considered them. ("What do you think about using a custom validator here?") 49 | - Seek to understand the author's perspective. 50 | - Sign off on the pull request with the approving tool of Github/Bitbucket . 51 | - Use and reference the recommended style guide of the technology. 52 | -------------------------------------------------------------------------------- /ruby/rspec/tutorial.md: -------------------------------------------------------------------------------- 1 | # RSpec Tutorial/Guide 2 | 3 | RSpec is a testing tool for Ruby, it is the most frequently used testing library in Ruby applications, and it was the source of inspiration for testing tools in other languages like jasmine and mocha. This tutorial will aim to steer you into the path of testing and show you the different ways of testing. 4 | 5 | There are some key parts in an RSpec test: 6 | * `describe`: it is an example group, usually groups the tests of a method or class. 7 | * `it`: the atom of the tests, it should contain one test/expectation. 8 | * `before`: hook that executes before a group of tests, depending where is located, if it’s located inside a describe or context, it will be executed before the tests inside the describe or context. By default runs before each test, but you can pass the option `:all` to run before all of them only. It’s usually used for testing setup 9 | * `after`: same as before, but after the tests, it’s usually used for teardown setup. 10 | * `context`: same behavior as `describe` but it’s used for a semantic purpose: each block of tests inside the context behaves in a certain way or under certain conditions. As an example, you can check it in model specs. 11 | * `let`: it's a helper method for variables. The value will be cached across multiple calls in the same example but not across examples. By default `let` variables are lazy. [Read more - let vs let!](./tutorial/let-vs-let.md). 12 | 13 | ## Model specs 14 | They are straightforward unit tests which tests small parts of the system, they shouldn’t test private methods, just model’s methods and callbacks. These unit tests follows the same rules as services, jobs, mailers and any other ruby classes, except controllers that needs some setup. 15 | 16 | ``` 17 | describe User do 18 | describe '#full_name' do 19 | let(:user) { create(:user) } 20 | 21 | it 'returns the correct name' do 22 | expect(user.full_name).to eq("#{user.first_name} #{user.last_name}") 23 | end 24 | end 25 | 26 | describe '.search' do 27 | let(:user) { create(:user) } 28 | 29 | context 'with the right term' do 30 | it 'returns a user' do 31 | expect(User.search(user.name)).to include user 32 | end 33 | end 34 | 35 | context 'with another term' do 36 | it 'returns 0 users' do 37 | expect(User.search('whatever')).to be_empty 38 | end 39 | end 40 | end 41 | end 42 | ``` 43 | 44 | As you can see, instance methods are described with `# `and class methods with `.` . 45 | 46 | ## Feature specs 47 | Feature specs are integration tests that aim to cover all the application flow, it starts when a user does something in the UI (like clicking a button) and finishes when the user sees the response (like an alert). Thus they are written from the user viewpoint, but in some cases you can check the Model/DB if there aren’t a visible response or to make sure the app is working as expected. 48 | 49 | We use Capybara for interacting with the browser and PhantomJS (through the ruby driver poltergeist) or webkit to test pages with js. You can take a look at the capybara’s GitHub page [about drivers](https://github.com/teamcapybara/capybara#drivers). If your test doesn't need Javascript in order to verify the behavior, don't enable it. An example of feature test is: 50 | 51 | ``` 52 | describe 'User creates a task' do 53 | before do 54 | visit new_task_path 55 | end 56 | 57 | it 'can create a task' do 58 | fill_in 'Title', with: 'My new task' 59 | fill_in 'Description', with: 'I need to do the laundry' 60 | click_button 'Create Task' 61 | 62 | expect(page).to have_content 'The task was created' 63 | end 64 | end 65 | ``` 66 | 67 | Also, Capybara comes with a built-in DSL for creating descriptive acceptance tests, where the previous code can be rewritten as: 68 | 69 | ``` 70 | feature 'User creates a task' do 71 | background do 72 | visit new_task_path 73 | end 74 | 75 | scenario 'can create a task' do 76 | fill_in 'Title', with: 'My new task' 77 | fill_in 'Description', with: 'I need to do the laundry' 78 | click_button 'Create Task' 79 | 80 | expect(page).to have_content 'The task was created' 81 | end 82 | end 83 | ``` 84 | 85 | Both ways are accepted but the first one is the most used in the company because the other types of testings use the same structure. We recommend checking [this guide](https://robots.thoughtbot.com/write-reliable-asynchronous-integration-tests-with-capybara) that brings some tips at the time of doing feature testing. 86 | 87 | 88 | ## Controller specs 89 | Although feature specs are great for acceptance tests, they are slower to run. So leave the edge case tests to your model or controller. However, don't use them for API test, you should be using requests specs for that. An example of it is the following: 90 | 91 | ``` 92 | describe RegistrationsController do 93 | describe 'POST create' do 94 | let(:email) { 'email@example.com' } 95 | let(:password) { 'somepassword' } 96 | 97 | it 'redirects to the dashboard' do 98 | post :create, { email: email, password: password } 99 | expect(response).to redirect_to '/dashboard' 100 | end 101 | 102 | it 'creates an user' do 103 | expect do 104 | post :create, { email: email, password: password } 105 | end.to change(User, :count).by(1) 106 | end 107 | 108 | it 'shows a successfully flash' do 109 | post :create, { email: email, password: password } 110 | expect(flash[:notice]).to match(/You can start using the app!/) 111 | end 112 | end 113 | end 114 | ``` 115 | 116 | ## Request specs 117 | Requests specs are the integration tests for APIs (but way faster), they are designed to drive behavior through the full stack. We recommend doing requests specs over controller specs for APIs because the routes and real responses are covered. An example of them are: 118 | 119 | ``` 120 | describe 'POST api/v1/users/sign_in', type: :request do 121 | let(:user) { create(:user, password: password) } 122 | let(:params) do { 123 | user: 124 | { 125 | email: user.email, 126 | password: password 127 | } 128 | } 129 | end 130 | 131 | context 'with correct params' do 132 | before do 133 | post new_user_session_path, params: params, as: :json 134 | end 135 | 136 | it 'returns success' do 137 | expect(response).to be_success 138 | end 139 | 140 | it 'returns the user' do 141 | expect(json[:user][:id]).to eq(user.id) 142 | expect(json[:user][:email]).to eq(user.email) 143 | end 144 | end 145 | end 146 | ``` 147 | 148 | ## Testing tools 149 | We use some tools that help and improve the testing experience, they are: 150 | 151 | * [FactoryBot](https://github.com/thoughtbot/factory_bot): You can use `Product.create` in your test, but the setup of it could be tedious to do everywhere, so we prefer moving the data setup to one place: the factories. You can achieve this with Rails fixtures, but you don’t have too much control over them. 152 | * [Database Cleaner](https://github.com/DatabaseCleaner/database_cleaner): The idea behind it is to maintain a clean state between tests, so you don't have to worry about any data left by a previous test. 153 | * [Faker](https://github.com/stympy/faker): It’s a library for generating fake data such as names, addresses, and phone numbers. 154 | * [Webmock](https://github.com/bblimke/webmock)/[vcr](https://github.com/vcr/vcr): It’s a library for stubbing HTTP request, you shouldn’t interact with real APIs during tests because you can reach API’s quota or make unexpected changes to real data. 155 | -------------------------------------------------------------------------------- /infrastructure/Heroku.md: -------------------------------------------------------------------------------- 1 | # Heroku Hosting Guideline 2 | 3 | ## Intro 4 | 5 | Heroku is the PaaS platform of choice for Rootstrap, even as, in order to support larger projects and offer greater value by reducing our customer's infrastructure costs we are driven to adopt larger cloud providers, primarily AWS in its multiple offerings: plain IaaS (EC2), PaaS (Elastic Beanstalk), or container-based platforms (ECS/EKS). 6 | This guide establishes a criteria for choosing Heroku as hosting platform for new projects and overall best practices around it. 7 | 8 | ### Table of Contents 9 | 10 | - [Intro](#intro) 11 | - [Pros](#pros) 12 | - [Cons](#cons) 13 | - [Heroku vs AWS](#heroku-vs-aws) 14 | - [When to use Heroku](#when-to-use-heroku) 15 | - [When to move to AWS](#when-to-move-to-aws) 16 | - [Comparison Tools](#estimation-and-comparison-tools) 17 | - [Best Practices](#best-practices) 18 | 19 | 20 | ### Pros 21 | * Very easy to get started 22 | * Free tier extends including most AddOns supports cheap PoC and MVP environments 23 | * Quick fixes based on customer feedback are deployed immediately 24 | * Setup can be implemented End2End without Linux Admin or DevOps expertise 25 | 26 | ### Cons 27 | * Cost rise quickly after basic usage tiers 28 | * No automated scalability out of the box (Addon solutions are also expensive) 29 | * Not really feasible for very compute-intensive projects 30 | * Deployments can be slow for larger apps 31 | * Not all tech stacks supported 32 | 33 | 34 | ## Heroku vs AWS 35 | 36 | ### When to use Heroku 37 | 38 | In order to deploy a new project into Heroku, most of these conditions should be met: 39 | 40 | * Initial scope of the project is just an MVP 41 | * Project will have low resource requirements (RAM, CPU) even in Production short/medium-term 42 | * The project is built on a well-known and fully supported stack, eg. 43 | * Language: 44 | * Python: Django/Flask 45 | * Ruby: Rails 4+ 46 | * Node.js 12+ 47 | * Database: 48 | * PostgreSQL 11+ 49 | * In-memory data store: 50 | * Redis 5 51 | * Email: 52 | * Sendgrid 53 | * Mailgun 54 | * Logging: 55 | * Papertrail 56 | * There are no DevOps hours allocated to the project 57 | * There is no available [Reference Architecture](https://www.notion.so/rootstrap/AWS-Architecture-5e8083e3968a45de9e240885a31921be) for AWS that fits the needs of the project 58 | 59 | ### When to move to AWS 60 | 61 | As a rule of thumb, when total ownership costs for the project in Heroku exceeds or are expected to exceed $350/month, it is worthwhile to invest in migrating to AWS. 62 | This is the estimated cost for an application including: 63 | * A Production environment with: 64 | * 4 (2 web, 2 workers) `Standard 2X` instances (1GB RAM, 2x CPU share) 65 | * 1 `Standard 0` PostgreSQL instance (4GB RAM, 64 GB storage, 120-connection limit) 66 | * 1 `Premium 1` Redis instance (100MB memory, 80-connection limit) 67 | * 1 `Tough Tiger` RabbitMQ instance (10 million messages/month) 68 | * 1 `Fixa` Papertrail instance (65MB/day, 7-day indexing, 365-day archiving) 69 | * 1 `Bronze` Sendgrid instance (40k emails/momth) 70 | * Small dev team (up to 5 collaborators) 71 | * Standard Heroku Support plan 72 | * A Staging environment with Hobby-level instances ($7/month) and free-tier data addons, with occasional upscaling (demo sessions, workshops, load testing) 73 | 74 | This budget is just above a comparable setup in AWS, using dedicated (prod) and shared (staging) EC2 and RDS instances with on-demand purchasing + SES for email, including network traffic costs, EBS-backed storage, CloudWatch basic monitoring, with Developer-tier AWS support plan. 75 | 76 | In addition, implementing recommended Reference Architectures in AWS should allow to reduce these costs, eg. by sharing larger instances and by combining reserved purchasing + spot purchasing for burst or async workloads 77 | 78 | Scaling to further capacity, high-availability setups or greater storage needs quickly expands the cost difference between platforms. 79 | 80 | ### Estimation and comparison tools 81 | Basic instance pricing: https://www.heroku.com/pricing 82 | 83 | Add-ons: https://elements.heroku.com/addons 84 | 85 | Amazon cost calculator tool: https://calculator.s3.amazonaws.com/index.html 86 | 87 | Amazon EC2/RDS instance comparison chart: https://www.ec2instances.info/ 88 | 89 | ## Best Practices 90 | 91 | Most configuration principles that apply to any cloud platform are valid here and should be elaborated further on the DevOps Hub. Some items which are particularly relevant for Heroku are worth mentioning here. 92 | 93 | #### Concurrent web servers 94 | Web applications that process concurrent requests make more efficient use of dyno resources than those that only process one request at a time. 95 | 96 | Official recommendations from Heroku are: 97 | * Ruby: [Puma](https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server) 98 | * Python: [Gunicorn](https://devcenter.heroku.com/articles/python-gunicorn) 99 | 100 | #### Officially supported buildpacks and tools 101 | Heroku has official buildpacks, among other languages, for: 102 | * [Ruby](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-ruby) 103 | * Rails 4.x and higher 104 | * Use Bundler for dependency management 105 | * [Python](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-python) 106 | * supported runtimes: 3.6, 3.7, 2.7(not recommended) 107 | * Django or Flask framwork 108 | * Celery for async tasks 109 | * Any other pip-installable packages are supported, except some with C dependencies 110 | * [Node](https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-nodejs) 111 | * supported runtimes: 13.x, 12.x, 10.x(not recommended) 112 | * dependencies installed from package.json using Yarn or npm 113 | 114 | #### Separate environments 115 | When maintaining more than one environment on Heroku (eg. Development, Test, Demo, Staging, Production), each should be configured as a separate app. 116 | 117 | If there are multiple repos that belong to the same project, each component should be deployed as an independent app with their own environment-specific deploys. 118 | 119 | [Pipelines](https://devcenter.heroku.com/articles/pipelines) can be used to promote the same build between environments, **except** if the application has a stateful build, ie compiles configuration variables into the deployed artifact (which regardled of the platform or deployment method must be avoided if at all possible). 120 | 121 | #### Continuous deployment from GitHub 122 | Configure GitHub integration for Heroku apps to enable automatic deploy upon specified branches: https://devcenter.heroku.com/articles/github-integration 123 | 124 | Integrations should be configured in a manner consistent with the [Git Workflow guide](../git/README.md), eg. 125 | * Every PR merged into `develop` should automatically be deployed to Development env 126 | * Deploy demo branches to specific environment for this purpose (Staging or Demo) 127 | * Deploy Releases (from either `develop` or `master`) automatically to Staging environment 128 | * Trigger deploys to Production manually and from release tags in `master` branch or by promoting the latest release on Staging using a pipeline. 129 | 130 | #### S3 for static files 131 | 132 | Application dynos have ephemeral filesystems, meaning a solution is required for hosting static durable files (eg React frontend apps). 133 | 134 | Amazon S3 is the preferred platform for serving static files due to its very low price, high availability and durability, and caching capabilities (using CloudFront). 135 | 136 | All supported languages in Heroku can use the S3 API for handling file uploads: https://devcenter.heroku.com/articles/s3 137 | -------------------------------------------------------------------------------- /flutter/README.md: -------------------------------------------------------------------------------- 1 | # Flutter Style Guide 2 | 3 | This Flutter style guide recommends best practices so that real-world Flutter 4 | programmers can write code that can be maintained by other real-world Flutter 5 | programmers. A style guide that reflects real-world usage gets used, while a 6 | style guide that holds to an ideal that has been rejected by the people it is 7 | supposed to help risks not getting used at all—no matter how good it is. 8 | 9 | ## Table of Contents 10 | 11 | * [Identifiers](#identifiers) 12 | * [Formatting](#formatting) 13 | * [Performance](#performance) 14 | * [Tests](#tests) 15 | 16 | ### Identifiers 17 | Identifiers come in three flavors in Dart. 18 | UpperCamelCase names capitalize the first letter of each word, including the first. 19 | lowerCamelCase names capitalize the first letter of each word, except the first which is always lowercase, even if it’s an acronym. 20 | lowercase_with_underscores names use only lowercase letters, even for acronyms, and separate words with _. 21 | 22 | - DO name types using UpperCamelCase. 23 | Linter rule: camel_case_types 24 | Classes, enum types, typedefs, and type parameters should capitalize the first letter of each word (including the first word), and use no separators. 25 | ``` 26 | class SliderMenu { ... } 27 | class HttpRequest { ... } 28 | typedef Predicate = bool Function(T value); 29 | ``` 30 | 31 | This even includes classes intended to be used in metadata annotations. 32 | ``` 33 | class Foo { 34 | const Foo([Object? arg]); 35 | } 36 | 37 | @Foo(anArg) 38 | class A { ... } 39 | 40 | @Foo() 41 | class B { ... } 42 | ``` 43 | 44 | If the annotation class’s constructor takes no parameters, you might want to create a separate lowerCamelCase constant for it. 45 | ``` 46 | const foo = Foo(); 47 | @foo 48 | class C { ... } 49 | ``` 50 | 51 | - DO name extensions using UpperCamelCase. 52 | Linter rule: camel_case_extensions 53 | Like types, extensions should capitalize the first letter of each word (including the first word), and use no separators. 54 | ``` 55 | extension MyFancyList on List { ... } 56 | extension SmartIterable on Iterable { ... } 57 | ``` 58 | 59 | - DO name import prefixes using lowercase_with_underscores. 60 | Linter rule: library_prefixes 61 | Good: 62 | ``` 63 | import 'dart:math' as math; 64 | import 'package:angular_components/angular_components.dart' as angular_components; 65 | import 'package:js/js.dart' as js; 66 | ``` 67 | 68 | Bad: 69 | ``` 70 | import 'dart:math' as Math; 71 | import 'package:angular_components/angular_components.dart' as angularComponents; 72 | import 'package:js/js.dart' as JS; 73 | ``` 74 | 75 | - DO name other identifiers using lowerCamelCase. 76 | Linter rule: non_constant_identifier_namesClass members, top-level definitions, variables, parameters, and named parameters should capitalize the first letter of each word except the first word, and use no separators. 77 | ``` 78 | var count = 3; 79 | HttpRequest httpRequest; 80 | void align(bool clearItems) { 81 | // ... 82 | } 83 | ``` 84 | 85 | - PREFER using lowerCamelCase for constant names. 86 | Linter rule: constant_identifier_names 87 | In new code, use lowerCamelCase for constant variables, including enum values. 88 | ``` 89 | const pi = 3.14; 90 | const defaultTimeout = 1000; 91 | final urlScheme = RegExp('^([a-z]+):'); 92 | class Dice { 93 | static final numberGenerator = Random(); 94 | } 95 | ``` 96 | 97 | - DO capitalize acronyms and abbreviations longer than two letters like words. 98 | Capitalized acronyms can be hard to read, and multiple adjacent acronyms can lead to ambiguous names. For example, given a name that starts with HTTPSFTP, there’s no way to tell if it’s referring to HTTPS FTP or HTTP SFTP. 99 | To avoid this, acronyms and abbreviations are capitalized like regular words. 100 | 101 | ``` 102 | class HttpConnection {} 103 | class DBIOPort {} 104 | class TVVcr {} 105 | class MrRogers {} 106 | var httpRequest = ... 107 | var uiHandler = ... 108 | var userId = ... 109 | Id id; 110 | ``` 111 | 112 | - PREFER using _, __, etc. for unused callback parameters. 113 | Sometimes the type signature of a callback function requires a parameter, but the callback implementation doesn’t use the parameter. In this case, it’s idiomatic to name the unused parameter _. If the function has multiple unused parameters, use additional underscores to avoid name collisions: __, ___, etc. 114 | 115 | ``` 116 | futureOfVoid.then((_) { 117 | //... 118 | }); 119 | ``` 120 | 121 | - DON’T use prefix letters. 122 | Hungarian notation and other schemes arose in the time of BCPL, when the compiler didn’t do much to help you understand your code. Because Dart can tell you the type, scope, mutability, and other properties of your declarations, there’s no reason to encode those properties in identifier names. 123 | ``` 124 | Good: defaultTimeout 125 | Bad: kDefaultTimeout 126 | ``` 127 | 128 | ### Formatting 129 | Like many languages, Dart ignores whitespace. However, humans don’t. Having a consistent whitespace style helps ensure that human readers see code the same way the compiler does. 130 | 131 | - AVOID lines longer than 80 characters: 132 | Linter rule: lines_longer_than_80_chars 133 | Readability studies show that long lines of text are harder to read because your eye has to travel farther when moving to the beginning of the next line. This is why newspapers and magazines use multiple columns of text. 134 | 135 | If you really find yourself wanting lines longer than 80 characters, our experience is that your code is likely too verbose and could be a little more compact. The main offender is usually VeryLongCamelCaseClassNames. Ask yourself, “Does each word in that type name tell me something critical or prevent a name collision?” If not, consider omitting it. 136 | 137 | Note that dart format does 99% of this for you, but the last 1% is you. It does not split long string literals to fit in 80 columns, so you have to do that manually. 138 | 139 | Exception: When a URI or file path occurs in a comment or string (usually in an import or export), it may remain whole even if it causes the line to go over 80 characters. This makes it easier to search source files for a path. 140 | 141 | Exception: Multi-line strings can contain lines longer than 80 characters because newlines are significant inside the string and splitting the lines into shorter ones can alter the program. 142 | 143 | - DO use curly braces for all flow control statements: 144 | Linter rule: curly_braces_in_flow_control_structures 145 | Doing so avoids the dangling else problem. 146 | ``` 147 | if (isWeekDay) { 148 | print('Bike to work!'); 149 | } else { 150 | print('Go dancing or read a book!'); 151 | } 152 | ``` 153 | 154 | Exception: When you have an if statement with no else clause and the whole if statement fits on one line, you can omit the braces if you prefer: 155 | 156 | ``` 157 | if (arg == null) return defaultValue; 158 | ``` 159 | 160 | If the body wraps to the next line, though, use braces: 161 | 162 | ``` 163 | if (overflowChars != other.overflowChars) { 164 | return overflowChars < other.overflowChars; 165 | } 166 | ``` 167 | 168 | Use raw string 169 | A raw string can be used to not come across escaping only backslashes and dollars. 170 | ``` 171 | //Do 172 | var s = r'This is demo string \ and $'; 173 | 174 | //Do not 175 | var s = 'This is demo string \\ and \$'; 176 | ``` 177 | 178 | - Use ternary operator for single-line cases. 179 | ``` 180 | String welcomeMessage = !isNewUser ? 'Welcome!' : 'Please sign up!'; 181 | ``` 182 | 183 | - Always highlight the type of member when its value type is known. Do not use var when it is not required. As var is a dynamic type takes more space and time to resolve. 184 | ``` 185 | //Do 186 | int item = 10; 187 | final Car bar = Car(); 188 | String name = 'john'; 189 | const int timeOut = 20; 190 | 191 | //Do not 192 | var item = 10; 193 | final car = Car(); 194 | const timeOut = 2000; 195 | ``` 196 | 197 | ### Performance 198 | * Minimize expensive operations: 199 | Some operations are more expensive than others, meaning that they consume more resources. Obviously, you want to only use these operations when necessary. How you design and implement your app’s UI can have a big impact on how efficiently it runs. 200 | 201 | Here are some things to keep in mind when designing your UI: 202 | - Avoid repetitive and costly work in build() methods since build() can be invoked frequently when ancestor widgets rebuild. 203 | - Avoid overly large single widgets with a large build() function. Split them into different widgets based on encapsulation but also on how they change. 204 | - When setState() is called on a State object, all descendent widgets rebuild. Therefore, localize the setState() call to the part of the subtree whose UI actually needs to change. Avoid calling setState() high up in the tree if the change is contained to a small part of the tree. 205 | - The traversal to rebuild all descendents stops when the same instance of the child widget as the previous frame is re-encountered. This technique is heavily used inside the framework for optimizing animations where the animation doesn’t affect the child subtree. See the TransitionBuilder pattern and the source code for SlideTransition, which uses this principle to avoid rebuilding its descendents when animating. 206 | - Always try to use const widgets. The widget will not change when setState call we should define it as constant. It will impede the widget from being rebuilt so it revamps performance. 207 | - To create reusable pieces of UIs, prefer using a StatelessWidget rather than a function. 208 | - Use MediaQuery/LayoutBuilder only when needed. 209 | - Use streams only when needed 210 | 211 | * Using SizedBox instead of Container in Flutter: 212 | There are multiple use cases where you will require to use a placeholder. Here is the ideal example below: 213 | ``` 214 | return _isNotLoaded ? Container() : YourAppropriateWidget(); 215 | ``` 216 | 217 | The main issue here is that Container doesn't have a const constructor and load a lot of business logic on it, on the other hand SizedBox is a const constructor and builds a fixed-size box. The width and height parameters can be null to specify that the size of the box should not be constrained in the corresponding dimension. 218 | 219 | Thus, when we have to implement the placeholder, SizedBox should be used rather than using a container. 220 | ``` 221 | return _isNotLoaded ? SizedBox() : YourAppropriateWidget(); 222 | ``` 223 | 224 | * Minimize use of opacity and clipping 225 | Opacity is another expensive operation, as is clipping. Here are some tips you might find to be useful: 226 | - Use the Opacity widget only when necessary. See the Transparent image section in the Opacity API page for an example of applying opacity directly to an image, which is faster than using the Opacity widget. 227 | - Instead of wrapping simple shapes or text in an Opacity widget, it’s usually faster to just draw them with a semitransparent color. (Though this only works if there are no overlapping bits in the to-be-drawn shape.) 228 | - To implement fading in an image, consider using the FadeInImage widget, which applies a gradual opacity using the GPU’s fragment shader. 229 | - Clipping doesn’t call saveLayer() (unless explicitly requested with Clip.antiAliasWithSaveLayer), so these operations aren’t as expensive as Opacity, but clipping is still costly, so use with caution. By default, clipping is disabled (Clip.none), so you must explicitly enable it when needed. 230 | - To create a rectangle with rounded corners, instead of applying a clipping rectangle, consider using the borderRadius property offered by many of the widget classes. 231 | 232 | 233 | * Implement grids and lists thoughtfully: 234 | How your grids and lists are implemented might be causing performance problems for your app. This section describes an important best practice when creating grids and lists, and how to determine whether your app uses excessive layout passes. 235 | When building a large grid or list, use the lazy builder methods, with callbacks. That ensures that only the visible portion of the screen is built at startup time. 236 | 237 | * Select libraries carefully: 238 | - Before using a third-party library take into account: 239 | Do you relly needed it? It makes more sense to write the code or copy the code than depend on a library. 240 | - When was the package updated? We should always avoid using stale packages. 241 | - How popular is the package? If the package has considerable popularity, it is much easier to find community support. 242 | - How frequently does the package get updated? This is really important if we want to take advantage of the latest Dart features. 243 | 244 | ### Tests 245 | * Write tests for critical functionality 246 | The contingencies of relying on manual testing will always be there, having an automated set of tests can help you save a notable amount of time and effort. As Flutter mainly targets multiple platforms, testing each and every functionality after every change would be time-consuming and call for a lot of repeated effort. 247 | 248 | Let’s face the facts, having 100% code coverage for testing will always be the best option, however, it might not always be possible on the basis of available time and budget. Nonetheless, it’s still essential to have at least tests to cover the critical functionality of the app. 249 | 250 | Unit and widget tests are the topmost options to go with from the very beginning and it’s not at all tedious as compared to integration tests. 251 | -------------------------------------------------------------------------------- /git/README.md: -------------------------------------------------------------------------------- 1 | # Git Workflow 2 | 3 | We use style guides and standards on a daily basis to help us keep a unified criterion regarding each type of technology. The main aim of this document is to create a git workflow standard for all the different projects in the company. 4 | 5 | There are many advantages that come with the use of the previously mentioned criteria. One of them is that it allows us to have the same nomenclature to make it easy to incorporate new team members as well as keeping a better project organization. Furthermore, it tackles common problems by providing effective solutions. Last but not least, the workflow focuses on keeping a tidy and reliable history that truly reflects the project’s current state. 6 | 7 | Below is listed the document content. Some of the instructions are mandatory and need to be followed throughout the development process of any project. The mandatory specifications are highlighted with a **[Required]** label. Even though some of the directions are not mandatory, they are highly recommended. 8 | 9 | 10 | [Spanish version](./README-es.md) 11 | 12 | 13 | ## Table of Contents 14 | 15 | - [Branches](#branches) 16 | - [Before releasing to production](#before-releasing-to-production) 17 | - [Develop branch (develop)](#develop-branch-develop-required) 18 | - [Staging branch (staging)](#staging-branch-staging-required) 19 | - [QA branch (qa)](#qa-branch-qa-optional) 20 | - [Feature branch (feature/x)](#feature-branch-featurex) 21 | - [Fix branch (fix/x)](#fix-branch-fixx) 22 | - [Enhancement branch (enhancement/x)](#enhancement-branch-enhancementx) 23 | - [Feedback branch (feedback/x)](#feedback-branch-feedbackx) 24 | - [Demo branch (demo)](#demo-branch-demo) 25 | - [Post production release](#post-production-release-) 26 | - [Master branch (master)](#master-branch-master-required) 27 | - [Release branch (release_branch_x)](#release-branch-release_branch_x-required) 28 | - [Integration of a Release branch to master and develop](#integration-of-a-release-branch-to-master-and-develop) 29 | - [Feedback branch (feedback/x)](#feedback-branch-feedbackx-1) 30 | - [Hotfix branch (hotfix/sign-up)](#hotfix-hotfixsign-up-required) 31 | - [Release management](#release-management) 32 | - [Before releasing to production](#before-releasing-to-production-1) 33 | - [After releasing to production](#after-releasing-to-production-required) 34 | - [First release to production](#first-release-to-production) 35 | - [Other releases to production](#other-releases-to-production) 36 | - [Major changes (MAJOR)](#major-changes-major) 37 | - [Minor changes (MINOR)](#minor-changes-minor) 38 | - [Bug fixes (PATCH)](#bug-fixes-patch) 39 | 40 | 41 | # Branches 42 | 43 | ## Before releasing to production 44 | 45 | This section contains aspects related to branch management before releasing to production. 46 | 47 | ### Develop branch (develop) **[Required]** 48 | 49 | This branch is the main one because it reflects the work done by the whole team during the development phase in a project. It is used as the base branch until the app is released to production. 50 | 51 | 52 | * **Origin:** *master* (in the initial project set up this branch is created from master) 53 | * **Destination:** - 54 | * **Use case:** Integration of the work done by the development team. 55 | 56 | ### Staging branch (staging) **[Required]** 57 | 58 | This branch has all the features ready to be tested by the Client. 59 | 60 | If the project has CD configured, every time something is merged into this branch the code will be automatically deployed to the Staging environment for the Client to test. 61 | 62 | * **Origin:** *master* (in the initial project set up this branch is created from master) 63 | * **Destination:** - 64 | * **Use case:** Integration of the work done by the development team which is already tested and approved by the QA team and ready for the Client to review. 65 | 66 | ### QA branch (qa) **[Optional]** 67 | 68 | This branch has all the features ready to be tested by the QA team. 69 | 70 | If the project has CD configured, every time something is merged into this branch the code will be automatically deployed to the QA environment. 71 | 72 | * **Origin:** *master* (in the initial project set up this branch is created from master) 73 | * **Destination:** *staging* 74 | * **Use case:** Integration of the work done by the development team ready for QA team to test. 75 | 76 | **_Note_**: *If a feature merged into the QA branch is not ready to be promoted to the Staging branch, then all needed changes (like: fixes, removing the feature, disabling access to it, etc) should be made in the Develop branch, followed by the promotion of those changes to the QA branch to continue the flow.* 77 | 78 | ### Feature branch (feature/x) 79 | 80 | This branch’s purpose is to develop a new feature. It has all the progress until the functionality being developed is completed. Pull requests are created from these type of branches and once they have passed the code review process, they have to be merged to develop. 81 | 82 | _Example: feature/sign-up_ 83 | 84 | * **Origin:** *develop* 85 | * **Destination:** *develop* 86 | * **Use case:** Develop a new feature during a sprint. 87 | 88 |

89 | 90 |

91 | 92 | ### Fix branch (fix/x) 93 | 94 | This branch is created in order to fix a bug during the development phase. 95 | 96 | _Example: fix/sign-up-error-messages_ 97 | 98 | * **Origin:** *develop* 99 | * **Destination:** *develop* 100 | * **Use case:** Fix a bug during development 101 | 102 | ### Enhancement branch (enhancement/x) 103 | 104 | This branch is created to add an enhancement during the development phase on something that was already merged to the development branch. 105 | 106 | _Example: enhancement/users-retrievement-query_ 107 | 108 | * **Origin:** *develop* 109 | * **Destination:** *develop* 110 | * **Use case:** Add an enhancement during development. 111 | 112 | ### Feedback branch (feedback/x) 113 | 114 | It is used to work on feedback provided by any team member or the PO about a previously developed feature. 115 | 116 | _Example: feedback/sign-up-change-inputs-order_ 117 | 118 | * **Origin:** *develop* 119 | * **Destination:** *develop* 120 | * **Use case:** Implement feedback given by the PO or any other team member about a specific feature previously developed 121 | 122 | ### Demo branch (demo) 123 | 124 | This branch has two main aims: 125 | 126 | 1. Avoid interference with code review process 127 | 2. Show the PO all the progress done by the team in a review session 128 | 129 | It is a temporal branch created from develop where all branches that are not ready to be integrated to develop are merged. The only purpose of this branch is to present things in the demo, so it should be immediately discarded after performing it. 130 | 131 | Changes in this branch will be reflected in develop after reviewed and approved in a code review process. 132 | 133 | * **Origin:** *develop* 134 | * **Destination:** - (delete branch) 135 | * **Use case:** Integration of all the features (completed or not) needed in a review session. A PR approved by code review is a requirement to merge a new feature to **_develop_**, sometimes, due to different reasons (incomplete or unreviewed code), new features are not ready on time to be presented to the PO in a demo. In order to solve this inconvenience, a temporal branch is created to be deployed or to create a build from it (depending on technology); there, all the branches to be shown in a review can be merged automatically without the need of a code review. 136 | 137 | If there is a change of any kind in the feature branches in a commit (*git amend*) or in the history (*git rebase*), a new *demo* branch can be created to reflect the change or it can just be merged in the existing branch. 138 | 139 | **_Note_**: *Despite the fact that this branch's main purpose is showing the progress in a review, it is not advised to send a version from that branch. In these cases you should talk to project or company referents.* 140 | 141 | *This branch can be deployed in an environment previous to staging such as development. See: Environment management base on the git workflow (work in progress).* 142 | 143 |

144 | 145 |

146 | 147 | ## Post production release 148 | 149 | This section’s purpose is explaining branch management after releasing to production. 150 | 151 | All the previously mentioned branches must be used in the same way they were used before release, except for those branches which are explained again under this section. 152 | 153 | The main purpose is to keep commits history as clean as possible. As a consequence, classical approaches based on *cherry-pick* and *merge* to keep branches up to date are replaced for the use of *rebase*. 154 | 155 | ### Master branch (master) **[Required]** 156 | 157 | It reflects the current code deployed in production, therefore, it must be stable. All the new features to be developed are going to have as origin *develop* and they are going to be merged to master once they are working as expected and approved by PO in the Staging environment. 158 | 159 | * **Origin:** - 160 | * **Destination:** - 161 | * **Use case:** Reflect production’s current state. 162 | 163 | ### Release branch (release_branch_x) **[Required]** 164 | 165 | Its main objective is to separate new features ready to be released to production from those that either the team or the client do not consider to be ready for release. As a consequence, this branch only contains features that are production ready. 166 | 167 | It should be deployed in a testing environment (usually staging) accessible to the client and all the feedback given by him/her must be included in this branch. 168 | 169 | The timing in which this branch should be created depends on the project and the team. In projects with big teams is convenient to create it when the development of the first feature in the version starts. However, when teams are small it is better to create it once all the features to be released are completed. This is the most common scenario in the company. 170 | 171 | * **Origin:** *develop* 172 | * **Destination:** *master and develop* 173 | * **Use case:** Merge feedback or fixes to code that were released to a testing environment. If there are bugs to fix or feedback to include after the branch has been released to a testing environment, they should be merged to this branch and not to *develop.* In this way, we avoid releasing future features that belong to a different release and were merged to *develop* by another developer in the team. Furthermore, it allows us to keep track of features we will release next. 174 | 175 | #### **Integration of a Release branch to master and develop** 176 | 177 | Once the release branch is ready to go to production it should be included in master and develop as well. 178 | 179 | **Master** 180 | 181 | It is directly included in **master** through a Pull Request created from master. Ideally, every commit in that PR passed through a code review process, consequently, another code review is unnecessary to merge it, the developer should just check everything is correct. 182 | 183 | **Develop** 184 | 185 | It is directly included in **develop** through a Pull Request created from the release branch. Everything said for the master branch applies here too. 186 | 187 | ### Feedback branch (feedback/x) 188 | 189 | This branch is created to work on feedback related to a specific feature. 190 | 191 | _Example: feedback/sign-up-change-inputs-order_ 192 | 193 | * **Origin:** *release branch* 194 | * **Destination:** *release branch* 195 | * **Use case:** Feedback from the client or any team member about a feature after its release branch has been deployed to a testing environment. 196 | 197 | ### Hotfix (hotfix/sign-up) **[Required]** 198 | 199 | This branch is created to solve bugs or critical changes that should not be mixed with the current development —*develop*— branch. 200 | 201 | Development bugs should not be confused with these bugs, development bugs should be merged to *develop or a release branch.* Not every bug deserves a hotfix, a hotfix aims to solve a critical bug that directly affects essential system functionality. 202 | 203 | It is the **developer's responsibility** to reflect that hotfix in develop by also submitting a Pull Request from this branch. 204 | 205 | * **Origin:** *master* 206 | * **Destination:** *master and develop* 207 | * **Use case:** Critical changes that affect the expected behaviour of the application in production. 208 | 209 |

210 | 211 |

212 | 213 | 214 | # Release management 215 | 216 | In this section, we explain how releases before and after production should be handled. 217 | 218 | **_What is a release?_** 219 | 220 | It is a link to a new version with notes describing all the changes implemented since the last version. 221 | 222 | These versions are handled with tags which are automatically created with every new release. Tags are immutable pictures of a branch. 223 | 224 | **_Advantages_** 225 | 226 | Version management sprint to sprint or post production is a way of keeping track of all the versions available in staging and specially in production, so we can better organize our work. 227 | 228 | ## Before releasing to production 229 | 230 | Once the sprint is finished and feedback from the client was included, a release is created. See [https://help.github.com/articles/creating-releases/](https://help.github.com/articles/creating-releases/) 231 | 232 | * **Origin:** develop 233 | * **Versión:** v0.x, x being the current sprint number. 234 | * **Title:** Release sprint x, x being the current sprint number. 235 | * **Description:** should contain a list with all the features developed during the sprint. 236 | 237 | _Note: version management before releasing to production is highly recommended but not mandatory._ 238 | 239 | ## After releasing to production **[Required]** 240 | 241 | Releases after production are the most important ones because they allow us to have an immutable image of the code in production. In this case, releases are always created from *master*. For example, after merging a hotfix or release branch into master. 242 | 243 | ### First release to production 244 | 245 | The first time the app is released to production, there must be a merge of all the content in *develop* to *master.* Then, the release is created. 246 | 247 | * **Origin:** master 248 | * **Version**: v1.0 249 | * **Title:** Release v1.0 250 | * **Description:** high level details of the features developed 251 | 252 | 253 | ### Other releases to production 254 | 255 | After the first release to production, it is recommendable to use the standard MAJOR.MINOR.PATCH. 256 | 257 | #### Major changes (MAJOR) 258 | 259 | Version in which incompatible changes with the previous version or a big number of features that represent a change at the business level are introduced. 260 | 261 | * **Origin:** master 262 | * **Version:** (vMAJOR + 1).0.0 263 | * **Title:** Release (vMAJOR + 1).0.0 264 | * **Description:** new features and minor bug fixes 265 | 266 | #### Minor changes (MINOR) 267 | 268 | When new functionalities and non-critical bug fixes are released we increase the MINOR value to generate a new version. 269 | 270 | * **Origin:** master 271 | * **Version:** vMAJOR.(MINOR + 1).0 272 | * **Title:** Release vMAJOR.(MINOR + 1).0 273 | * **Description:** new features and minor bug fixes 274 | 275 | 276 | #### Bug fixes (PATCH) 277 | 278 | It contains urgent and non-urgent bug fixes. If bugs do not require immediate action, we wait until some bugs are fixed to release a new version. When a bug is urgent a new release must be created. 279 | 280 | * **Origin:** master 281 | * **Version:** vMAJOR.MINOR.(PATCH + 1) 282 | * **Title:** Release vMAJOR.MINOR.(PATCH + 1) 283 | * **Description:** fixed bugs that are important 284 | -------------------------------------------------------------------------------- /ruby/rspec/style_guide.md: -------------------------------------------------------------------------------- 1 | # RSpec style guide 2 | 3 | This RSpec style guide outlines our recommended best practices so that our developers can write code that can be maintained by other (future) developers. 4 | This is meant to be a style guide that reflects real-world usage, holding to an ideal that has been agreed upon by many of the people it was intended to be used by. 5 | 6 | 7 | ## Style Guide Rules 8 | 9 | ### Line Returns after `feature`, `context`, or `describe` 10 | 11 | Do not leave line returns after `feature`, `context` or `describe` 12 | descriptions. It makes the code more difficult to read and lowers the 13 | value of logical chunks. 14 | 15 | #### Bad Example 16 | 17 | ```ruby 18 | describe Article do 19 | 20 | describe '#summary' do 21 | 22 | context 'when there is a summary' do 23 | 24 | it 'returns the summary' do 25 | # ... 26 | end 27 | end 28 | end 29 | end 30 | ``` 31 | 32 | #### Good Example 33 | 34 | ```ruby 35 | describe Article do 36 | describe '#summary' do 37 | context 'when there is a summary' do 38 | it 'returns the summary' do 39 | # ... 40 | end 41 | end 42 | end 43 | end 44 | ``` 45 | 46 | ### `let`, `subject`, and `before`/`after` group line returns 47 | 48 | Leave one line return after `let`, `subject`, and `before`/`after` blocks. 49 | 50 | #### Bad Example 51 | 52 | ```ruby 53 | describe Article do 54 | subject { create(:some_article) } 55 | describe '#summary' do 56 | # ... 57 | end 58 | end 59 | ``` 60 | 61 | #### Good Example 62 | 63 | ```ruby 64 | describe Article do 65 | subject { create(:some_article) } 66 | 67 | describe '#summary' do 68 | # ... 69 | end 70 | end 71 | ``` 72 | 73 | ### `let`, `subject`, `before`/`after` grouping 74 | 75 | Only group `let`, `subject` blocks and separate them from `before`/`after` 76 | blocks. It makes the code much more readable. 77 | 78 | #### Bad Example 79 | 80 | ```ruby 81 | describe Article do 82 | subject { create(:some_article) } 83 | let(:user) { create(:user) } 84 | before do 85 | # ... 86 | end 87 | after do 88 | # ... 89 | end 90 | describe '#summary' do 91 | # ... 92 | end 93 | end 94 | ``` 95 | 96 | #### Good Example 97 | 98 | ```ruby 99 | describe Article do 100 | subject { create(:some_article) } 101 | let(:user) { create(:user) } 102 | 103 | before do 104 | # ... 105 | end 106 | 107 | after do 108 | # ... 109 | end 110 | 111 | describe '#summary' do 112 | # ... 113 | end 114 | end 115 | ``` 116 | 117 | ### `it` block line returns 118 | 119 | Leave one line return around `it` blocks. This helps to separate the 120 | expectations from their conditional logic (contexts for instance). 121 | 122 | #### Bad Example 123 | 124 | ```ruby 125 | describe '#summary' do 126 | let(:item) { double('something') } 127 | 128 | it 'returns the summary' do 129 | # ... 130 | end 131 | it 'does something else' do 132 | # ... 133 | end 134 | it 'does another thing' do 135 | # ... 136 | end 137 | end 138 | ``` 139 | 140 | #### Good Example 141 | 142 | ```ruby 143 | describe '#summary' do 144 | let(:item) { double('something') } 145 | 146 | it 'returns the summary' do 147 | # ... 148 | end 149 | 150 | it 'does something else' do 151 | # ... 152 | end 153 | 154 | it 'does another thing' do 155 | # ... 156 | end 157 | end 158 | ``` 159 | 160 | ### `before(:each)`or `before` 161 | 162 | There is no need to specify `(:each)` for `before`/`after` blocks, as it is 163 | the default functionality. The expression `before(:all)` should be rarely used, 164 | but if you find a case where it is necessary, just write it out as `before(:all)` 165 | 166 | #### Bad Example 167 | 168 | ```ruby 169 | describe '#summary' do 170 | before(:each) do 171 | subject.summary = 'something' 172 | end 173 | end 174 | ``` 175 | 176 | #### Good Example 177 | 178 | ```ruby 179 | describe '#summary' do 180 | before do 181 | subject.summary = 'something' 182 | end 183 | end 184 | ``` 185 | 186 | ### 'should' it or 'should not' in `it` statements 187 | 188 | Do not write 'should' or 'should not' at the beginning of your `it` blocks. 189 | The descriptions represent actual functionality - not what might be happening. 190 | 191 | #### Bad Example 192 | 193 | ```ruby 194 | it 'should return the summary' do 195 | # ... 196 | end 197 | ``` 198 | 199 | #### Good Example 200 | 201 | ```ruby 202 | it 'returns the summary' do 203 | # ... 204 | end 205 | ``` 206 | 207 | ### The One Expectation 208 | 209 | Use only one expectation per example. There are very few scenarios where two 210 | or more expectations in a single `it` block should be used. So, general rule 211 | of thumb is one expectation per `it` block. 212 | 213 | #### Bad Example 214 | 215 | ```ruby 216 | describe ArticlesController do 217 | #... 218 | 219 | describe 'GET new' do 220 | it 'assigns new article and renders the new article template' do 221 | get :new 222 | expect(assigns[:article]).to be_a(Article) 223 | expect(response).to render_template :new 224 | end 225 | end 226 | 227 | # ... 228 | end 229 | ``` 230 | 231 | #### Good Example 232 | 233 | ```ruby 234 | describe ArticlesController do 235 | #... 236 | 237 | describe 'GET new' do 238 | it 'assigns a new article' do 239 | get :new 240 | expect(assigns[:article]).to be_a(Article) 241 | end 242 | 243 | it 'renders the new article template' do 244 | get :new 245 | expect(response).to render_template :new 246 | end 247 | end 248 | end 249 | ``` 250 | 251 | ### Context Cases 252 | 253 | `context` blocks should pretty much always have an opposite negative case. It 254 | should actually be a strong code smell if there is a single context (without a 255 | matching negative case) that needs refactoring, or may have no purpose. 256 | 257 | #### Bad Example 258 | 259 | ```ruby 260 | # This is a case where refactoring is the correct choice 261 | describe '#attributes' do 262 | context 'the returned hash' do 263 | it 'includes the display name' do 264 | # ... 265 | end 266 | 267 | it 'includes the creation time' do 268 | # ... 269 | end 270 | end 271 | end 272 | 273 | # This is a case where the negative case needs to be tested, but wasn't 274 | describe '#attributes' do 275 | context 'when display name is present' do 276 | before do 277 | subject.display_name = 'something' 278 | end 279 | 280 | it 'includes the display name' do 281 | # ... 282 | end 283 | end 284 | end 285 | ``` 286 | 287 | #### Good Example 288 | 289 | ```ruby 290 | # Refactored 291 | describe '#attributes' do 292 | subject { create(:article) } 293 | 294 | expect(subject.attributes).to include subject.display_name 295 | expect(subject.attributes).to include subject.created_at 296 | end 297 | 298 | # Negative case added 299 | describe '#attributes' do 300 | context 'when display name is present' do 301 | before do 302 | subject.display_name = 'something' 303 | end 304 | 305 | it 'includes the display name' do 306 | # ... 307 | end 308 | end 309 | 310 | context 'when display name is not present' do 311 | before do 312 | subject.display_name = nil 313 | end 314 | 315 | it 'does not include the display name' do 316 | # ... 317 | end 318 | end 319 | end 320 | ``` 321 | 322 | ### `context` descriptions 323 | 324 | `context` block descriptions should always start with 'when' or ‘with’, and be in the 325 | form of a sentence with proper grammar. 326 | 327 | #### Bad Example 328 | 329 | ```ruby 330 | context 'the display name not present' do 331 | # ... 332 | end 333 | ``` 334 | 335 | #### Good Example 336 | 337 | ```ruby 338 | context 'when the display name is not present' do 339 | # ... 340 | end 341 | ``` 342 | 343 | ### `it` descriptions 344 | 345 | `it` block descriptions should never end with a conditional. This is a code 346 | smell that advices the `it` most likely needs to be wrapped in a `context`. This also happens when the description is too long. 347 | 348 | #### Bad Example 349 | 350 | ```ruby 351 | it 'returns the display name if it is present' do 352 | # ... 353 | end 354 | ``` 355 | 356 | #### Good Example 357 | 358 | ```ruby 359 | context 'when display name is present' do 360 | it 'returns the display name' 361 | end 362 | 363 | # This encourages the addition of negative test cases that might have 364 | # been overlooked 365 | context 'when display name is not present' do 366 | it 'returns nil' 367 | end 368 | ``` 369 | 370 | ### `describe` block naming 371 | 372 | - use hash `#method` for instance methods 373 | - use dot `.method` for class methods 374 | 375 | Given the following exists 376 | 377 | ```ruby 378 | class Article 379 | def summary 380 | #... 381 | end 382 | 383 | def self.latest 384 | #... 385 | end 386 | end 387 | ``` 388 | 389 | #### Bad Example 390 | 391 | ```ruby 392 | describe Article do 393 | describe 'summary' do 394 | #... 395 | end 396 | 397 | describe 'latest' do 398 | #... 399 | end 400 | end 401 | ``` 402 | 403 | #### Good Example 404 | 405 | ```ruby 406 | describe Article do 407 | describe '#summary' do 408 | #... 409 | end 410 | 411 | describe '.latest' do 412 | #... 413 | end 414 | end 415 | ``` 416 | 417 | ### `it` in iterators 418 | 419 | Do not write iterators to generate tests. When another developer adds a 420 | feature to one of the items in the iteration, he must then break it out into a 421 | separate test - he is forced to edit code that has nothing to do with his pull 422 | request. Use shared examples instead. 423 | 424 | #### Bad Example 425 | 426 | ```ruby 427 | [:new, :show, :index].each do |action| 428 | it 'returns 200' do 429 | get action 430 | expect(response).to be_ok 431 | end 432 | end 433 | ``` 434 | 435 | #### Good Example 436 | 437 | more verbose for the time being, but better for the future development 438 | 439 | ```ruby 440 | 441 | shared_examples 'responds successfully' do 442 | it 'returns 200' do 443 | get method 444 | expect(response).to be_ok 445 | end 446 | end 447 | 448 | describe 'GET new' do 449 | let(:method) { 'new' } 450 | 451 | it_behaves_like 'responds successfully' 452 | end 453 | 454 | describe 'GET show' do 455 | let(:method) { 'show' } 456 | 457 | it_behaves_like 'responds successfully' 458 | end 459 | 460 | describe 'GET index' do 461 | let(:method) { 'index' } 462 | 463 | it_behaves_like 'responds successfully' 464 | end 465 | ``` 466 | 467 | ### Factories/Fixtures 468 | 469 | Use [FactoryBot](https://github.com/thoughtbot/factory_bot) to create test 470 | objects in integration tests. You should very rarely have to use 471 | `ModelName.create` within an integration spec. Do **not** use fixtures as they 472 | are not nearly as maintainable as factories. 473 | 474 | ```ruby 475 | subject { create(:some_article) } 476 | ``` 477 | 478 | ### Mocks/Stubs/Doubles 479 | 480 | Use mocks and stubs with caution. While they help to improve the performance 481 | of the test suite, you can mock/stub yourself into a false-positive state very 482 | easily. When resorting to mocking and stubbing, only mock against a small, 483 | stable, obvious (or documented) API, so stubs are likely to represent reality 484 | after future refactoring. 485 | 486 | This generally means you should use them with more isolated/behavioral 487 | tests rather than with integration tests. 488 | 489 | ```ruby 490 | # double an object 491 | article = double('article') 492 | 493 | # stubbing a method 494 | allow(Article).to receive(:find).with(5).and_return(article) 495 | ``` 496 | 497 | *NOTE*: if you stub a method that could give a false-positive test result, you 498 | have gone too far. See below: 499 | 500 | #### Bad Example 501 | 502 | ```ruby 503 | subject { double('article') } 504 | 505 | describe '#summary' do 506 | context 'when summary is not present' do 507 | # This stubbing of the #nil? method, makes the test pass, but 508 | # you are no longer testing the functionality of the code, 509 | # you are testing the functionality of the test suite. 510 | # This test would pass if there was not a single line of code 511 | # written for the Article class. 512 | it 'returns nil' do 513 | summary = double('summary') 514 | allow(subject).to receive(:summary).and_return(summary) 515 | allow(summary).to receive(:nil?).and_return(true) 516 | expect(subject.summary).to be_nil 517 | end 518 | end 519 | end 520 | ``` 521 | 522 | #### Good Example 523 | 524 | ```ruby 525 | subject { double('article') } 526 | 527 | describe '#summary' do 528 | context 'when summary is not present' do 529 | # This is no longer stubbing all of the functionality, and will 530 | # actually test the objects handling of the methods return value. 531 | it 'returns nil' do 532 | allow(subject).to receive(:summary).and_return(nil) 533 | expect(subject.summary).to be_nil 534 | end 535 | end 536 | end 537 | ``` 538 | 539 | ### Dealing with Time 540 | 541 | Always use [Timecop](https://github.com/travisjeffery/timecop) instead of 542 | stubbing anything on Time or Date. 543 | 544 | #### Bad Example 545 | 546 | ```ruby 547 | it 'offsets the time 2 days into the future' do 548 | current_time = Time.now 549 | allow(Time).to receive(:now).and_return(current_time) 550 | expect(subject.get_offset_time).to be_the_same_time_as (current_time + 2.days) 551 | end 552 | ``` 553 | 554 | #### Good Example 555 | 556 | ```ruby 557 | it 'offsets the time 2 days into the future' do 558 | Timecop.freeze(Time.now) do 559 | expect(subject.get_offset_time).to eq 2.days.from_now 560 | end 561 | end 562 | ``` 563 | 564 | 565 | ### `let` blocks 566 | 567 | Use `let` blocks instead of `before(:each)` blocks to create data for the spec 568 | examples. `let` blocks get lazily evaluated. It also removes the instance 569 | variables from the test suite (which don't look as nice as local variables). 570 | 571 | These should primarily be used when you have duplication among a number of 572 | `it` blocks within a `context` but not all of them. Be careful with overuse of 573 | `let` as it makes the test suite much more difficult to read. 574 | 575 | ```ruby 576 | # use this: 577 | let(:article) { create(:article) } 578 | 579 | # ... instead of this: 580 | before { @article = create(:article) } 581 | ``` 582 | 583 | ### `subject` 584 | 585 | Use `subject` when possible 586 | 587 | ```ruby 588 | describe Article do 589 | subject { create(:article) } 590 | 591 | it 'is not published on creation' do 592 | expect(subject).not_to be_published 593 | end 594 | end 595 | ``` 596 | 597 | ### Magic Matchers 598 | 599 | Use RSpec's 'magical matcher' methods when possible. For instance, a class 600 | with the method `published?` should be tested with the following: 601 | 602 | ```ruby 603 | it 'is published' do 604 | # actually tests subject.published? == true 605 | expect(subject).to be_published 606 | end 607 | ``` 608 | 609 | ### Incidental State 610 | 611 | Avoid incidental state as much as possible. 612 | 613 | #### Bad Example 614 | 615 | ```ruby 616 | it 'publishes the article' do 617 | article.publish 618 | 619 | # Creating another shared Article test object above would cause this 620 | # test to break 621 | expect(Article.count).to eq(2) 622 | end 623 | ``` 624 | 625 | #### Good Example 626 | 627 | ```ruby 628 | it 'publishes the article' do 629 | expect { article.publish }.to change(Article, :count).by(1) 630 | end 631 | ``` 632 | 633 | ### DRY 634 | 635 | Be careful not to focus on being 'DRY' by moving repeated expectations into a shared environment too early, as this can lead to brittle tests that rely too much on one another. 636 | 637 | Generally, is it best to start doing everything directly in your `it` 638 | blocks even if it is duplication and then refactor your tests once they are working, to be a little more DRY. 639 | However, keep in mind that duplication in test suites is NOT frowned upon, 640 | in fact it is preferred if it provides easier understanding and reading of a test. 641 | 642 | 643 | # Credit 644 | 645 | We based this guide on [reachlocal/rspec-style-guide](https://github.com/reachlocal/rspec-style-guide) 646 | -------------------------------------------------------------------------------- /git/README-es.md: -------------------------------------------------------------------------------- 1 | # Git Workflow 2 | 3 | En nuestro trabajo diario aplicamos diferentes guías de estilos y estandarizaciones que nos ayudan a tener un criterio unificado en relacion a diferentes tecnologías. El objetivo de este documento es crear un estándar a nivel empresa para el workflow de git en los diferentes proyectos. 4 | 5 | Los lineamientos descriptos en este documento tienen variadas ventajas. Entre ellas, nos permite manejar la misma nomenclatura facilitando la incorporación de otros integrantes al equipo y promueve la mejor organización de los proyectos. Asimismo, ayuda a definir soluciones para algunos problemas habituales. Finalmente, el workflow planteado esta enfocado a mantener un historial confiable que refleje el estado del proyecto. 6 | 7 | A continuación, listaremos el contenido de este documento. Algunos de los lineamientos son de carácter obligatorio y deben ser seguidos en el desarrollo de cualquier proyecto. Los contenidos de carácter obligatorio serán resaltados con la etiqueta **[Required]**. Si bien el resto de los lineamientos no son obligatorios, son altamente recomendables. 8 | 9 | * [Versión en Inglés](./README.md) 10 | 11 | 12 | ## Contenido 13 | 14 | - [Manejo de branches](#manejo-de-branches) 15 | - [Previo salir a producción](#previo-salir-a-producci%C3%B3n) 16 | - [Develop branch (develop)](#develop-branch-develop--requerida) 17 | - [Staging branch (staging)](#staging-branch-staging-requerida) 18 | - [QA branch (qa)](#qa-branch-qa-opcional) 19 | - [Feature branch (feature/x)](#feature-branch-featurex) 20 | - [Fix branch (fix/x)](#fix-branch-fixx) 21 | - [Enhancement branch (enhancement/x)](#enhancement-branch-enhancementx) 22 | - [Feedback branch (feedback/x)](#feedback-branch-feedbackx) 23 | - [Demo branch (demo)](#demo-branch-demo) 24 | - [Post salida a producción](#post-salida-a-producción) 25 | - [Master branch (master)](#master-branch-master-required) 26 | - [Release branch (release_branch_x)](#release-branch-release_branch_x-required) 27 | - [Integración de Release branch a master y develop](#integraci%C3%B3n-de-release-branch-a-master-y-develop) 28 | - [Feedback branch (feedback/x)](#feedback-feedbackx) 29 | - [Hotfix branch (hotfix/sign-up)](#hotfix-hotfixsign-up-requerida) 30 | - [Manejo de Releases](#manejo-de-releases) 31 | - [Previo salir a producción](#previo-salir-a-producci%C3%B3n-1) 32 | - [Post salida a producción](#post-salida-a-producción-required) 33 | - [Primera salida a producción](#primera-salida-a-producci%C3%B3n) 34 | - [Posteriores salidas a producción](#posteriores-salidas-a-producci%C3%B3n) 35 | - [Nueva versión (Major)](#nueva-versión-major) 36 | - [Arreglo de bugs (PATCH)](#arreglo-de-bugs-patch) 37 | - [Cambios menores (MINOR)](#cambios-menores-minor) 38 | 39 | 40 | # Manejo de branches 41 | 42 | 43 | ## Previo salir a producción 44 | 45 | En esta sección se explica el manejo de cada una de las branches involucradas en el proceso de desarrollo previo a salir a producción. 46 | 47 | 48 | ### Develop branch (develop) **[Requerida]** 49 | 50 | Es la branch principal, refleja la integración de todo el trabajo del equipo durante la etapa de desarrollo del proyecto. 51 | Es la branch base hasta salir a producción por primera vez. 52 | 53 | * **Origen:** *master* (cuando se inicia el proyecto, es creada desde master) 54 | * **Destino:** - 55 | * **Casos de uso:** Integración del trabajo del equipo. 56 | 57 | 58 | ### Staging branch (staging) **[Requerida]** 59 | 60 | Esta branch contiene todas las funcionalidades listas para ser testeadas por el Cliente. 61 | 62 | Si el proyecto tiene CD configurado, cada vez que algo sea mergeado en esta branch el código será automáticamente deployado al ambiente de Staging para que el Cliente pueda probarlo. 63 | 64 | * **Origen:** *master* (cuando se inicia el proyecto, es creada desde master) 65 | * **Destino:** - 66 | * **Casos de uso:** Integración del trabajo hecho por el equipo de desarrollo ya testeado y aprobado por el equipo de QA, listo para ser revisado por el Cliente. 67 | 68 | ### QA branch (qa) **[Opcional]** 69 | 70 | Esta branch contiene todas las funcionalidades listas para ser testeadas por el equipo de QA. 71 | 72 | Si el proyecto tiene CD configurado, cada vez que algo sea mergeado en esta branch el código será automáticamente deployado al ambiente de QA. 73 | 74 | * **Origen:** *master* (cuando se inicia el proyecto, es creada desde master) 75 | * **Destino:** *staging* 76 | * **Casos de uso:** Integración del trabajo hecho por el equipo de desarrollo listo para ser testeado por el equipo de QA. 77 | 78 | **_Nota_**: *Si una de las funcionalidades mergeadas en QA no está lista para ser promovida a la branch Staging, los ajustes necesarios (eliminar la funcionalidad/deshabilitar el acceso a la misma/etc) deben ser realizados en la branch Develop, para luego ser promovidos a la branch QA y así continuar con el flujo.* 79 | 80 |

81 | 82 |

83 | 84 | ### Feature branch (feature/x) 85 | 86 | Esta branch contiene una funcionalidad o parte de una en la que se está trabajando actualmente. Una vez completado el trabajo, se crea un Pull Request a develop y luego de finalizar el proceso de review es mergeada a develop. 87 | 88 | _Ejemplo: feature/sign-up_ 89 | 90 | * **Origen:** *develop* 91 | * **Destino:** *develop* 92 | * **Casos de uso:** Funcionalidad durante desarrollo. 93 | 94 | 95 | ### Fix branch (fix/x) 96 | 97 | Esta branch es creada para el arreglo de un bug durante la etapa de desarrollo. 98 | 99 | _Ejemplo: fix/sign-up-error-messages_ 100 | 101 | * **Origen:** *develop* 102 | * **Destino:** *develop* 103 | * **Casos de uso:** Bug durante desarrollo. 104 | 105 | 106 | ### Enhancement branch (enhancement/x) 107 | 108 | Esta branch es creada para agregar una mejora durante la etapa de desarrollo sobre alguna funcionalidad que ha sido mergeada previamente a la branch development. 109 | 110 | _Ejemplo: enhancement/users-retrievement-query_ 111 | 112 | * **Origen:** *develop* 113 | * **Destino:** *develop* 114 | * **Casos de uso:** Agregar mejora durante desarrollo. 115 | 116 | 117 | ### Feedback branch (feedback/x) 118 | 119 | Esta branch es creada para trabajar en feedback de una determinada funcionalidad. 120 | 121 | _Ejemplo: feedback/sign-up-change-inputs-order_ 122 | 123 | * **Origen:** *develop* 124 | * **Destino:** *develop* 125 | * **Casos de uso:** Feedback del cliente o cualquier integrante del equipo de una determinada funcionalidad durante desarrollo. 126 | 127 | 128 | ### Demo branch (demo) 129 | 130 | Esta branch tiene dos objetivos: 131 | 132 | 1. No afectar el proceso de code review. 133 | 2. Mostrar el avance del equipo en la review con el cliente. 134 | 135 | Branch temporal creada a partir de develop en la cual se mergea el contenido de otras branches que aún no estan prontas para ser integradas a develop. El único propósito de esta branch es enteramente dedicado a la demo, por lo que debe ser descartada inmediatamente despues de la realización de la misma. 136 | 137 | * **Origen:** *develop* 138 | * **Destino:** - (borrar branch) 139 | * **Casos de uso:** Mostrar todas las funcionalidades desarrolladas hasta el momento en la instancia de review, no importa si ya estan completadas o no. 140 | Para poder mergear una branch a develop, es obligatorio que la misma pase por un adecuado proceso de code review, sin embargo, es frecuente que al momento de la review, la branch aun no este completa o el PR aun no haya sido aprobado. Para esto, todas las branches que van a ser mostradas en la review, pueden ser mergeadas automáticamente en la branch temporal demo, sin necesidad de code review. 141 | 142 | Si se realizan cambios en las branches, se puede mergear de nuevo ese cambio en la branch *demo* o crear de nuevo la branch temporal si hay un cambio en un commit (*git amend*) o en la historia (*git rebase*). 143 | 144 | *Nota: a pesar de ser una branch que tiene como finalidad mostrar el avance en una review, no es recomendable enviar una versión a partir de esta branch. En estos casos, hablar con el referente del proyecto o algunos de los referentes de la empresa.* 145 | 146 | *Es recomendable hacer el deploy de esta branch a un ambiente previo a staging, como puede ser development. Ver: manejo de environments en base a git workflow (documento en construcción).* 147 | 148 |

149 | 150 |

151 | 152 | 153 | ## Post salida a producción 154 | 155 | En esta sección se explica el manejo de cada una de las branches involucradas en el proceso de desarrollo post salida a producción. 156 | 157 | A no ser que la branch sea nuevamente explicada en esta sección, las anteriores branches detalladas en la sección *Previo salir a producción*, deben seguir siendo utilizadas de la misma manera. 158 | 159 | Con el manejo de branches detallado a continuación, se busca mantener el historial de commits, siempre y cuando sea posible. 160 | Por esta razón, para poder mantener las branches actualizadas entre si, no se utilizan enfoques clásicos basados en el uso de *cherry-pick* y *merge*. De lo contrario, se prioriza el uso de *rebase*. 161 | 162 | 163 | ### Master branch (master) **[Required]** 164 | 165 | Esta branch refleja el estado de producción, siempre debe estar en un estado estable. 166 | 167 | Por esta razón, las nuevas funcionalidades continúan siendo desarrolladas a partir de *develop* y son integradas a master una vez que se encuentran funcionando y son aprobadas por el cliente en un ambiente de prueba. 168 | 169 | * **Origen:** - 170 | * **Destino:** - 171 | * **Casos de uso:** Reflejar producción 172 | 173 | 174 | ### Release branch (release_branch_x) **[Required]** 175 | 176 | Esta branch es creada conteniendo las nuevas funcionalidades que quieren ser agregadas al ambiente de producción. 177 | 178 | Su función es separar el desarrollo de nuevas funcionalidades que quieren ser agregadas a producción de otras que aún no están prontas o simplemente aún no quieren ser liberadas por el equipo o el cliente. 179 | 180 | Debe ser liberada por primera vez a un ambiente de prueba utilizado por el cliente y el feedback proveniente del mismo será agregado a esta branch. 181 | 182 | El momento en que debe ser creada depende del proyecto y el equipo. 183 | En equipos con varios integrantes es recomendando crearla al momento que se comienza a desarrollar la primer feature de la versión. 184 | Por otra parte, en equipos con pocos integrantes es recomendable crearla una vez completadas las funcionalidades que quieren ser liberadas. Este es el caso más común en la empresa. 185 | 186 | * **Origen:** *develop* 187 | * **Destino:** *master* 188 | * **Casos de uso:** Luego de liberada la branch a un ambiente de prueba, puede existir feedback del cliente o encontrarse bugs. Estos arreglos deben ir a la release branch y no a develop. 189 | De esta manera, si en simultáneo otro dev comenzó a trabajar en funcionalidades futuras y lo integró a *develop*, se evita liberar funcionalidades que no pertenecían a la release actual. Además, nos permite mantener un orden en el tiempo en relación a que vamos a liberar próximamente. 190 | 191 | 192 | #### Integración de Release branch a master y develop 193 | 194 | Luego de que la release branch está pronta para ser liberada a producción, la misma debe ser integrada a master y también a develop. 195 | 196 | ***Master*** 197 | 198 | Directamente a través de un Pull Request que es creado desde la release a **master**. Idealmente, cada uno de los commit de este Pull Request ya pasaron por un proceso de code review, por lo cual, no es necesario repetir el review para mergear, simplemente asegurarse que todo se encuentra de acuerdo a lo planeado y funcionando correctamente. 199 | 200 | ***Develop*** 201 | 202 | La release branch, puede además contener commits que no se encuentran en develop, comúnmente, aquellos que surgieron luego de feedback del cliente y fueron integrados a la release y no a develop. 203 | 204 | Para mantener **develop** actualizado, se debe hacer un rebase con *origin master* y luego un push a develop desde develop local. 205 | 206 | A continuación se detallan los comandos adecuados: 207 | 208 | 1. `git checkout develop` 209 | 2. `git pull origin develop:develop` para así actualizar *develop* local con *origin/develop*. 210 | 3. `git rebase -i origin/master` para actualizar el historial de develop con respecto a origin/master. 211 | 4. `git push origin develop:develop` para actualizar origin/develop. 212 | 213 | 214 | ### Feedback (feedback/x) 215 | 216 | Esta branch es creada para trabajar en el feedback de una funcionalidad. 217 | 218 | _Ejemplo: feedback/sign-up-change-inputs-order_ 219 | 220 | * **Origen:** *release branch* 221 | * **Destino:** *release branch* 222 | * **Casos de uso:** Feedback del cliente o interno de una determinada funcionalidad luego de liberada una release branch a un ambiente de prueba. 223 | 224 | 225 | ### Hotfix (hotfix/sign-up) **[Requerida]** 226 | 227 | Estas branches son no planeadas, son creadas para solucionar bugs o cambios críticos, que no deben ser mezclados con el actual desarrollo (*develop* branch). 228 | 229 | No confundir con bugs de desarrollo, estos últimos van contra *develop* o una release branch. Además, no todos los bugs son hotfixes, solo aquellos que se priorizan como críticos y afectan directamente una funcionalidad importante del sistema. 230 | 231 | El cambio también debe estar presente en develop. Por lo cual, es **responsabilidad del desarrollador** **que creó el commit** que una vez que el Pull Request es mergeado en master, **inmediatamente** el commit también se encuentra en la branch *develop.* 232 | 233 | Para esto, se deben seguir los mismos pasos detallados en la subsección **integración de release branch a master y develop** para la branch develop. 234 | 235 | * **Origen:** *master* 236 | * **Destino:** *master* 237 | * **Casos de uso:** Bugs o cambios críticos de producción. 238 | 239 |

240 | 241 |

242 | 243 | 244 | # Manejo de Releases 245 | 246 | En esta sección se explica cómo manejar releases previo y post salida a producción. Comenzando con una breve explicación del porqué y las ventajas que nos brinda el manejo de las mismas. 247 | 248 | **_¿Que es?_** 249 | 250 | Una Release es un link a una versión con un conjunto de notas que describen los cambios con respecto a la última versión. 251 | 252 | Las versiones se manejan utilizando tags, las cuales se crean automáticamente al crear una release. Las tags son fotos inmutables de una branch. 253 | 254 | **_Ventajas_** 255 | 256 | Manejar versiones sprint a sprint o post salida a producción nos permite manejar el trabajo de forma ordenada y tener un trackeo confiable de que versiones se encuentran en staging y principalmente en producción. 257 | 258 | 259 | ## Previo salir a producción 260 | 261 | Una vez finalizado el sprint y solucionado el feedback proveniente del cliente se crea una release. 262 | Ver [https://help.github.com/articles/creating-releases/](https://help.github.com/articles/creating-releases/) 263 | 264 | * **Origen:** develop 265 | * **Versión:** v0.x, donde x es el número del sprint en el cual se está trabajando. 266 | * **Título:** Release sprint x, donde x es el número del sprint. 267 | * **Descripción:** detalle de las funcionalidades desarrolladas durante desarrollo. 268 | 269 | _Nota: el manejo de releases previo salir a producción es altamente recomendado pero no obligatorio._ 270 | 271 | 272 | ## Post salida a producción **[Required]** 273 | 274 | El manejo de Releases luego de salir a producción es por demás importante dado que nos permite tener una captura inmutable de lo que hay en producción. 275 | En este caso, la tag se crea desde *master*. Por ejemplo, luego de mergear una branch hotfix o release a master. 276 | 277 | 278 | ### Primera salida a producción 279 | 280 | La primera vez que que se libera a producción se debe mergear el contenido de la branch *develop* en *master*. Luego, la release es creada. 281 | 282 | * **Origen:** master 283 | * **Versión:** v1.0 284 | * **Título:** Release v1.0 285 | * **Descripción:** detalle de las funcionalidades desarrolladas durante desarrollo. 286 | 287 | 288 | ### Posteriores salidas a producción 289 | 290 | En posteriores releases a producción se recomienda el estándar MAJOR.MINOR.PATCH. 291 | 292 | 293 | #### Nueva versión (Major) 294 | 295 | Se liberan un conjunto de funcionalidades que no son compatibles con la versión anterior o un conjunto muy grande de funcionalidades que pueden marcar un gran cambio desde el punto de vista del negocio. 296 | 297 | * **Origen:** master 298 | * **Versión** : (vMAJOR + 1).0.0 299 | * **Título:** Release (vMAJOR + 1).0.0 300 | * **Descripción:** descripción general de la nueva versión y detalle de las nuevas funcionalidades desarrolladas 301 | 302 | 303 | #### Cambios menores (MINOR) 304 | 305 | Versión que introduce cambios compatibles con la versión anterior. 306 | En este caso, dado que se liberan un conjunto de nuevas funcionalidades y arreglo de bugs no críticos, se incrementa en uno el valor MINOR para generar la nueva versión. 307 | 308 | * **Origen:** master 309 | * **Versión** : vMAJOR.(MINOR + 1).0 310 | * **Título:** Release vMAJOR.(MINOR + 1).0 311 | * **Descripción:** detalle de las nuevas funcionalidades desarrolladas y bugs que fueron solucionados. 312 | 313 | 314 | #### Arreglo de bugs (PATCH) 315 | 316 | Contiene uno o más arreglos de bugs. Independientemente de si los mismos son urgentes o no. 317 | Comúnmente, si los bugs no son urgentes, se espera a solucionar un conjunto de bugs para liberar una nueva versión. 318 | En caso de que el bug sea urgente, es necesario liberar una versión cuanto antes. 319 | 320 | * **Versión** : vMAJOR.MINOR.(PATCH + 1) 321 | * **Título:** Release vMAJOR.MINOR.(PATCH + 1) 322 | * **Descripción:** detalle de los bugs arreglados que son considerados importantes. 323 | -------------------------------------------------------------------------------- /kotlin/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Kotlin Style Guide 3 | 4 | This Kotlin style guide recommends best practices so that real-world Kotlin 5 | programmers can write code that can be maintained by other real-world Kotlin 6 | programmers. A style guide that reflects real-world usage gets used, while a 7 | style guide that holds to an ideal that has been rejected by the people it is 8 | supposed to help risks not getting used at all—no matter how good it is. 9 | 10 | 11 | ## Table of Contents 12 | 13 | * [Source file names](#source-file-names) 14 | * [Function names](#function-names) 15 | * [Property names](#property-names) 16 | * [Formatting](#formatting) 17 | * [Horizontal whitespace](#horizontal-whitespace) 18 | * [Colon](#colon) 19 | * [Class header](#class-header-formatting) 20 | * [Annotation](#annotation-formatting) 21 | * [Functions](#function-formatting) 22 | * [Expression body](#expression-body-formatting) 23 | * [Properties](#property-formatting) 24 | * [Control flow statements](#formatting-control-flow-statements) 25 | * [Method calls](#method-call-formatting) 26 | * [Lambdas](#lambda-formatting) 27 | * [Documentation/comments](#documentation-comments) 28 | * [Type aliases](#type-aliases) 29 | * [Conditional statements](#using-conditional-statements) 30 | * [Extension functions](#using-extension-functions) 31 | * [Scope functions](#scope-functions) 32 | * [run](#run) 33 | * [let](#let) 34 | * [apply](#apply) 35 | * [also](#also) 36 | * [with](#with) 37 | * [Combining scope functions](#combining-scope-functions) 38 | 39 | --------------------- 40 | 41 | ### Source file names: 42 | If a Kotlin file contains a single class (potentially with related top-level 43 | declarations), its name should be the same as the name of the class, with the 44 | .kt extension appended. If a file contains multiple classes or only top-level 45 | declarations, choose a name describing what the file contains, and name the 46 | file accordingly. Use the camel case with an uppercase first letter (for example, 47 | ProcessDeclarations.kt ) 48 | 49 | ### Function names: 50 | Names of functions, properties, and local variables start with a lower case 51 | letter and use the camel case and no underscores: 52 | 53 | ``` 54 | fun processDeclarations() { /*...*/ } 55 | var declarationCount = 1 56 | ``` 57 | 58 | Exception: factory functions used to create instances of classes can have the 59 | same name as the class being created: 60 | 61 | ``` 62 | abstract class Foo { /*...*/ } 63 | class FooImpl : Foo { /*...*/ } 64 | fun FooImpl(): Foo { return FooImpl() } 65 | ``` 66 | 67 | In tests (and only in tests), it's acceptable to use method names with spaces 68 | enclosed in backticks.(Note that such method names are currently not supported 69 | by the Android runtime.) Underscores in method names are also allowed in test 70 | code. 71 | 72 | ``` 73 | class MyTestCase { 74 | @Test fun `ensure everything works`() { /*...*/ } 75 | @Test fun ensureEverythingWorks_onAndroid() { /*...*/ } 76 | } 77 | ``` 78 | 79 | ### Property names: 80 | Names of constants (properties marked with const, top-level or object val 81 | properties with no custom get) should use uppercase snake case: 82 | 83 | ``` 84 | const val MAX_COUNT = 8 85 | val USER_NAME_FIELD = "UserName" 86 | ``` 87 | 88 | Names of top-level or object properties which hold objects with behavior or 89 | mutable data should use camel-case names: 90 | 91 | ``` 92 | val mutableCollection: MutableSet = HashSet() 93 | ``` 94 | 95 | Names of properties holding references to singleton objects can use the same 96 | naming style as object declarations: 97 | 98 | ``` 99 | val PersonComparator: Comparator = /*...*/ 100 | ``` 101 | 102 | For enum constants, it's OK to use either uppercase snake case 103 | ( enum class Color { RED, GREEN } ) or regular camel-case names starting with 104 | an uppercase first letter, depending on the usage. 105 | 106 | ### Formatting: 107 | Use 4 spaces for indentation. Do not use tabs. For curly braces, put the opening 108 | brace in the end of the line where the construct begins, and the closing brace on 109 | a separate line aligned horizontally with the opening construct. 110 | 111 | ``` 112 | if (elements != null) { 113 | for (element in elements) { 114 | // ... 115 | } 116 | } 117 | ``` 118 | 119 | ### Horizontal whitespace: 120 | Put spaces around binary operators ( a + b ). 121 | Exception: don't put spaces around the "range to" operator ( 0..i ). 122 | Do not put spaces around unary operators ( a++ ) 123 | Put spaces between control keywords ( if , when , for and while ) and the 124 | corresponding opening parenthesis. Do not put a space before an opening parenthesis 125 | in a primary constructor declaration, method declaration or method call. 126 | 127 | ``` 128 | class A(val x: Int) 129 | fun foo(x: Int) { ... } 130 | fun bar() { foo(1) } 131 | ``` 132 | 133 | - Never put a space after '(', ',', '[' , or before ']' , ')' 134 | - Never put a space around '.' or '?.' : 135 | foo.bar().filter { it > 2 }.joinToString() , foo?.bar() 136 | - Put a space after '//' : 137 | // This is a comment 138 | - Do not put spaces around angle brackets used to specify type parameters: 139 | class Map { ... } 140 | - Do not put spaces around '::' : 141 | Foo::class , String::length 142 | - Do not put a space before ? used to mark a nullable type: 143 | String? 144 | 145 | ### Colon 146 | Put a space before ':' in the following cases: 147 | - when it's used to separate a type and a supertype. 148 | - when delegating to a superclass constructor or a different constructor of the 149 | same class. 150 | - After the object keyword. 151 | - Don't put a space before ':' when it separates a declaration and its type. 152 | - Always put a space after ':' . 153 | 154 | ``` 155 | abstract class Foo : IFoo { 156 | abstract fun foo(a: Int): T 157 | } 158 | 159 | class FooImpl : Foo() { 160 | constructor(x: String) : this(x) { /*...*/ } 161 | val x = object : IFoo { /*...*/ } 162 | } 163 | ``` 164 | 165 | ### Class header formatting: 166 | Classes with a few primary constructor parameters can be written in a single line: 167 | 168 | ``` 169 | class Person(id: Int, name: String) 170 | ``` 171 | 172 | Classes with longer headers should be formatted so that each primary constructor 173 | parameter is in a separate line with indentation. Also, the closing parenthesis 174 | should be on a new line. If we use inheritance, then the superclass constructor 175 | call or list of implemented interfaces should be located on the same line as the 176 | parenthesis: 177 | 178 | ``` 179 | class Person( 180 | id: Int, 181 | name: String, 182 | surname: String 183 | ) : Human(id, name) { /*...*/ } 184 | ``` 185 | 186 | For multiple interfaces, the superclass constructor call should be located first and 187 | then each interface should be located in a different line: 188 | 189 | ``` 190 | class Person( 191 | id: Int, 192 | name: String, 193 | surname: String 194 | ) : Human(id, name), 195 | KotlinMaker { /*...*/ } 196 | ``` 197 | 198 | For classes with a long supertype list, put a line break after the colon and 199 | align all supertype names horizontally: 200 | 201 | ``` 202 | class MyFavouriteVeryLongClassHolder : 203 | MyLongHolder(), 204 | SomeOtherInterface, 205 | AndAnotherOne { 206 | 207 | fun foo() { /*...*/ } 208 | } 209 | ``` 210 | 211 | To clearly separate the class header and body when the class header is long, either put 212 | a blank line following the class header (as in the example above), or put the opening 213 | curly brace on a separate line: 214 | 215 | ``` 216 | class MyFavouriteVeryLongClassHolder : 217 | MyLongHolder(), 218 | SomeOtherInterface, 219 | AndAnotherOne 220 | { 221 | fun foo() { /*...*/ } 222 | } 223 | ``` 224 | 225 | Use regular indent (4 spaces) for constructor parameters. 226 | 227 | ### Annotation formatting: 228 | Annotations are typically placed on separate lines, before the declaration to 229 | which they are attached, and with the same indentation: 230 | 231 | ``` 232 | @Target(AnnotationTarget.PROPERTY) 233 | annotation class JsonExclude 234 | ``` 235 | 236 | Annotations without arguments may be placed on the same line: 237 | 238 | ``` 239 | @JsonExclude @JvmField 240 | var x: String 241 | ``` 242 | 243 | A single annotation without arguments may be placed on the same line as the 244 | corresponding declaration: 245 | 246 | ``` 247 | @Test fun foo() { /*...*/ } 248 | ``` 249 | 250 | ### Function formatting: 251 | If the function signature doesn't fit on a single line, use the following syntax: 252 | 253 | ``` 254 | fun longMethodName( 255 | argument: ArgumentType = defaultValue, 256 | argument2: AnotherArgumentType 257 | ): ReturnType { 258 | // body 259 | } 260 | ``` 261 | 262 | Use regular indent (4 spaces) for function parameters. 263 | 264 | Prefer using an expression body for functions with the body consisting of a single 265 | expression. 266 | 267 | ``` 268 | // bad 269 | fun foo(): Int { 270 | return 1 271 | } 272 | 273 | // good 274 | fun foo() = 1 275 | ``` 276 | 277 | ### Expression body formatting: 278 | If the function has an expression body that doesn't fit in the same line as the 279 | declaration, put the = sign on the first line. Indent the expression body by 4 280 | spaces. 281 | 282 | ``` 283 | fun f(x: String) = 284 | x.length 285 | ``` 286 | 287 | ### Property formatting: 288 | For very simple read-only properties, consider one-line formatting: 289 | 290 | ``` 291 | val isEmpty: Boolean get() = size == 0 292 | ``` 293 | 294 | For more complex properties, always put get and set keywords on separate lines: 295 | 296 | ``` 297 | val foo: String 298 | get() { /*...*/ } 299 | ``` 300 | 301 | For properties with an initializer, if the initializer is long, add a line break 302 | after the equals sign and indent the initializer by four spaces: 303 | 304 | ``` 305 | private val defaultCharset: Charset? = 306 | EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file) 307 | ``` 308 | 309 | ### Formatting control flow statements: 310 | If the condition of an if or when the statement is multiline, always use curly 311 | braces around the body of the statement. Indent each subsequent line of the 312 | condition by 4 spaces relative to statement begin. Put the closing parentheses 313 | of the condition together with the opening curly brace on a separate line: 314 | 315 | ``` 316 | if (!component.isSyncing && 317 | !hasAnyKotlinRuntimeInScope(module) 318 | ) { 319 | return createKotlinNotConfiguredPanel(module) 320 | } 321 | ``` 322 | 323 | Put the else , catch , finally keywords, as well as the while keyword of a do/while 324 | loop, on the same line as the preceding curly brace: 325 | 326 | ``` 327 | if (condition) { 328 | // body 329 | } else { 330 | // else part 331 | } 332 | 333 | 334 | try { 335 | // body 336 | } finally { 337 | // cleanup 338 | } 339 | ``` 340 | 341 | In a when statement, if a branch is more than a single line, consider separating it 342 | from adjacent case blocks with a blank line: 343 | 344 | ``` 345 | private fun parsePropertyValue(propName: String, token: Token) { 346 | when (token) { 347 | is Token.ValueToken -> 348 | callback.visitValue(propName, token.value) 349 | Token.LBRACE -> { 350 | // … 351 | } 352 | } 353 | } 354 | ``` 355 | 356 | Put short branches on the same line as the condition, without braces. 357 | 358 | ``` 359 | when (foo) { 360 | true -> bar() // good 361 | false -> { baz() } // bad 362 | } 363 | ``` 364 | 365 | ### Method call formatting: 366 | In long argument lists, put a line break after the opening parenthesis. Indent 367 | arguments by 4 spaces. Group multiple closely related arguments on the same line. 368 | 369 | ``` 370 | drawSquare( 371 | x = 10, y = 10, 372 | width = 100, height = 100, 373 | fill = true ) 374 | ``` 375 | 376 | Put spaces around the = sign separating the argument name and value. 377 | 378 | ### Lambda formatting: 379 | In lambda expressions, spaces should be used around the curly braces, as well as 380 | around the arrow which separates the parameters from the body. 381 | If a call takes a single lambda, it should be passed outside of parentheses whenever 382 | possible. 383 | 384 | ``` 385 | list.filter { it > 10 } 386 | ``` 387 | 388 | If assigning a label for a lambda, do not put a space between the label and the opening 389 | curly brace: 390 | 391 | ``` 392 | fun foo() { 393 | ints.forEach list@{ 394 | // ... 395 | } 396 | } 397 | ``` 398 | 399 | When declaring parameter names in a multiline lambda, put the names on the first line, 400 | followed by the arrow and the newline: 401 | 402 | ``` 403 | appendCommaSeparated(properties) { 404 | prop -> val propertyValue = prop.get(obj) // ... 405 | } 406 | ``` 407 | 408 | If the parameter list is too long to fit on a line, put the arrow on a separate line: 409 | 410 | ``` 411 | foo { 412 | context: Context, 413 | environment: Env 414 | -> 415 | context.configureEnv(environment) 416 | } 417 | ``` 418 | 419 | ### Documentation comments: 420 | For longer documentation comments, place the opening /** on a separate line and begin 421 | each subsequent line with an asterisk: 422 | 423 | ``` 424 | /** 425 | * This is a documentation comment 426 | * on multiple lines. 427 | */ 428 | ``` 429 | 430 | Short comments can be placed on a single line: 431 | 432 | ``` 433 | /** This is a short documentation comment. */ 434 | ``` 435 | 436 | String templates: 437 | Don't use curly braces when inserting a simple variable into a string template. 438 | Use curly braces only for longer expressions. 439 | 440 | ``` 441 | println("$name has ${children.size} children") 442 | ``` 443 | 444 | ### Type aliases: 445 | If you have a functional type or a type with type parameters which is used multiple times 446 | in a codebase, prefer deDning a type alias for it: 447 | 448 | ``` 449 | typealias PersonIndex = Map 450 | ``` 451 | 452 | ### Using conditional statements: 453 | Prefer using the expression form of try, if and when. Examples: 454 | 455 | ``` 456 | return if (x) foo() else bar() 457 | 458 | return when(x) { 459 | 0 -> "zero" 460 | else -> "nonzero" 461 | } 462 | ``` 463 | 464 | The above is preferable to: 465 | 466 | ``` 467 | if (x) 468 | return foo() 469 | else 470 | return bar() 471 | 472 | when(x) { 473 | 0 -> return "zero" 474 | else -> return "nonzero" 475 | } 476 | ``` 477 | 478 | ### Loops on ranges: 479 | Use the until function to loop over an open range: 480 | 481 | ``` 482 | for (i in 0..n - 1) { /*...*/ } // bad 483 | for (i in 0 until n) { /*...*/ } // good 484 | ``` 485 | 486 | ### Using extension functions: 487 | Use extension functions liberally. Every time you have a function that works primarily 488 | on an object, consider making it an extension function accepting that object as a receiver. 489 | To minimize API pollution, restrict the visibility of extension functions as much as it 490 | makes sense. As necessary, use local extension functions, member extension functions, 491 | or top-level extension functions with private visibility. 492 | 493 | ### Scope functions: 494 | Kotlin provides a variety of functions to execute a block of code in the context of a 495 | given object: let, run, with, apply, and also. There is not a specific rule for each one, 496 | you use them to have clean and easy to maintain code, and since Kotlin is a compiled 497 | language, with the use of those functions, you can get better performance of your apps. 498 | 499 | #### run 500 | Receive 'this' as an instance of the current context and return a final value, should be 501 | used to execute actions over the same context. 502 | 503 | ``` 504 | val dog = Dog() 505 | val result = dog.run { //this as instance of the object dog 506 | jump() // same as dog.jump() also you can use this.jump() 507 | sit() 508 | eat() 509 | sleep() // the final return value 510 | } 511 | ``` 512 | 513 | Without "run" the code looks like: 514 | 515 | ``` 516 | dog.jump() 517 | dog.sit() 518 | dog.eat() 519 | val result = dog.sleep() 520 | ``` 521 | 522 | #### let 523 | Receive 'it' as an instance of the current object and return a final value. 524 | It should be used to use the object context several times. 525 | 526 | ``` 527 | dog.let { 528 | renameDog(it, "Canelo") 529 | paintDog(it, red) 530 | } 531 | ``` 532 | 533 | #### apply 534 | Receives 'this' as an instance of the current context and return the object context. 535 | It should be used to change context data: 536 | 537 | ``` 538 | dog.apply { 539 | name = "Canelo" 540 | color = "Red" 541 | size = "Big" 542 | } 543 | ``` 544 | 545 | #### also 546 | Receive 'it' as an instance of the object and return the same context as result. 547 | It should be used to execute a code after a creation of the object or parameters 548 | assignations: 549 | 550 | ``` 551 | val dog = Dog().also { 552 | selectedDog = it 553 | } 554 | ``` 555 | 556 | - also can be combined with apply: 557 | 558 | ``` 559 | dog.apply { 560 | name = "Canelo" 561 | color = "Red" 562 | size = "Big" 563 | }.also { 564 | selectedDog = it 565 | } 566 | ``` 567 | 568 | #### with 569 | Similar to let, but using 'this' as context. It should be used if you know the 570 | object is not null: 571 | 572 | ``` 573 | val dog = Dog() 574 | with(dog) { 575 | renameDog(this, "Canelo") 576 | eat() 577 | } 578 | ``` 579 | 580 | But if you need to check the object nullability you should use 'let': 581 | 582 | ``` 583 | dog?.let { 584 | ... 585 | } 586 | ``` 587 | 588 | #### Combining scope functions 589 | Imagine you have two objects and you need to use a scope function inside another, 590 | you need to specify the name of the context to avoid context overlapping. 591 | 592 | ``` 593 | dog1.let { // it as instance of dog1 594 | dog2.let { d2 -> // you have to rename the context for dog2 595 | it.isBrotherOf(d2) //it stell an instance of dog1 596 | doSomething(arrayListOf(it,d2)) 597 | } 598 | // d2 doesn't exist in this context 599 | it.jump() 600 | } 601 | ``` 602 | -------------------------------------------------------------------------------- /python/cookiecutter-django.md: -------------------------------------------------------------------------------- 1 | # How to start a new Django project with Cookiecutter 2 | The aim of this guide is to help you to initialize your Django project from one of the most recognized open-source utilities for this area. 3 | 4 | ## Install Cookiecutter 5 | [*Cookiecutter*](https://github.com/cookiecutter/cookiecutter) is a command-line tool that helps us to create projects from different templates, in our case, we will create a Django project. 6 | 7 | Firstly, we have to globally install that tool running `$ pip install cookiecutter` 8 | 9 | ## Create a new project 10 | Since we want to create a Django project, we need to use the [Cookiecutter Django](https://github.com/pydanny/cookiecutter-django) template. 11 | 12 | Go to your development directory (the location where you want to create the root folder of the project) and run the following command to start the creation: 13 | `$ cookiecutter gh:pydanny/cookiecutter-django` 14 | 15 | ## Fill generator options 16 | *Cookiecutter* gives us a set of pre-defined configurations to include at our project. Each configuration depends on each project that you will be involved in. You can take a look at [the full list of generation options](https://cookiecutter-django.readthedocs.io/en/latest/project-generation-options.html). 17 | 18 | Despite the previously mentioned, we provide you a list of the principal configuration that you have to set in a new *Cookiecutter* project. 19 | 20 | NOTE: 21 | - Each option suggests a default value if you agree with it, just press enter. 22 | 23 | Some interesting options: 24 | - **project_name**: Write the name of the new project using an upper case at the beginning. For ex.: Cookiecutter Starter. 25 | - **project_slug**: Write the name of the new project using downcases and underscores. At Rootstrap we use the same name of the code repository. For ex.: cookiecutter_starter. 26 | - **description**: Write a short description of the new project. Remember to add it to the readme file. For ex.: This project is an example of how to configure a Django project from the beginning with Cookiecutter Django 27 | - **author_name**: Write the company name. For ex.: Rootstrap, Inc. 28 | - **domain_name**: Write the customer domain name. For ex.: example.com 29 | - **email**: Write the customer email contact. For ex.: info@example.com 30 | - **open_source_license**: Choose the License according to the final purpose of the project. If it's an open-source project, you should select the option `1` (MIT). But if it's a client project, you should select the option `5` (Not open source). 31 | - **windows**: This option asks us if we want to be able to develop at Windows environment. At Rootstrap we use Linux ones, so, you should enter the option: `n`. 32 | - **use_pycharm**: This option asks us if we want to add some configuration files to run the project at PyCharm, the Jetbrains' Python IDE. It's personal, choose `n` or `y` according to your preferences. 33 | - **cloud_provider**: Choose the cloud provider for files resources that will be attached from the models. At Rootstrap we use AWS, so, you should select option `1`. 34 | - **mail_service**: Choose the email service for send emails from the application. At Rootstrap we use Sendgrid, so, you should select option `6`. 35 | - **use_drf**: This option asks us if we want to add [Django REST framework](https://www.django-rest-framework.org/) to our application. At Rootstrap we use it as the base of our API, so, you should enter the option: `y`. 36 | - **use_mailhog**: This option asks us if we want to add [Mailhog](https://github.com/mailhog/MailHog) as an email viewer during the development process. We recommend that option, so, you should enter: `y`. 37 | - **use_heroku**: This option asks us if we want to add some configuration files to deploy the project to *Heroku*. At Rootstrap we usually deploy to it, so, you should enter: `y`. 38 | - **keep_local_envs_in_vcs**: This option asks us if we want to add the environment configurations to the Version Control System. Also, it's useful to set Heroku or docker variables. At Rootstrap we manage that information differently, so, you should enter: `n`. 39 | 40 |
41 | View full bash output. (Click ▶ to display the file) 42 | 43 | ```bash 44 | project_name [My Awesome Project]: Cookiecutter Starter 45 | project_slug [cookiecutter_starter]: 46 | description [Behold My Awesome Project!]: This proyect is an example of how configure a Django proyect since beginning 47 | author_name [Daniel Roy Greenfeld]: Rootstrap, Inc. 48 | domain_name [example.com]: example.com 49 | email [rootstrap@example.com]: info@example.com 50 | version [0.1.0]: 51 | Select open_source_license: 52 | 1 - MIT 53 | 2 - BSD 54 | 3 - GPLv3 55 | 4 - Apache Software License 2.0 56 | 5 - Not open source 57 | Choose from 1, 2, 3, 4, 5 [1]: 1 58 | timezone [UTC]: 59 | windows [n]: 60 | use_pycharm [n]: 61 | use_docker [n]: 62 | Select postgresql_version: 63 | 1 - 12.3 64 | 2 - 11.8 65 | 3 - 10.8 66 | 4 - 9.6 67 | 5 - 9.5 68 | Choose from 1, 2, 3, 4, 5 [1]: 1 69 | Select js_task_runner: 70 | 1 - None 71 | 2 - Gulp 72 | Choose from 1, 2 [1]: 73 | Select cloud_provider: 74 | 1 - AWS 75 | 2 - GCP 76 | 3 - None 77 | Choose from 1, 2, 3 [1]: 1 78 | Select mail_service: 79 | 1 - Mailgun 80 | 2 - Amazon SES 81 | 3 - Mailjet 82 | 4 - Mandrill 83 | 5 - Postmark 84 | 6 - Sendgrid 85 | 7 - SendinBlue 86 | 8 - SparkPost 87 | 9 - Other SMTP 88 | Choose from 1, 2, 3, 4, 5, 6, 7, 8, 9 [1]: 6 89 | use_async [n]: 90 | use_drf [n]: y 91 | custom_bootstrap_compilation [n]: n 92 | use_compressor [n]: 93 | use_celery [n]: 94 | use_mailhog [n]: y 95 | use_sentry [n]: 96 | use_whitenoise [n]: 97 | use_heroku [n]: y 98 | Select ci_tool: 99 | 1 - None 100 | 2 - Travis 101 | 3 - Gitlab 102 | Choose from 1, 2, 3 [1]: 103 | keep_local_envs_in_vcs [y]: n 104 | debug [n]: 105 | ``` 106 | 107 |
108 | 109 | 110 | Note: By default the root folder of the project generated will be equal to the entered project_slug, that is, the name of the main Django app. After this process, you can change the root folder project name if you want. 111 | 112 | ## Install pipenv 113 | The recommendation from Rootstrap is that you use [`pipenv`](https://github.com/pypa/pipenv) to manage the development environment dependencies. 114 | 115 | To install the package globally, just run `$ pip3 install pipenv` 116 | 117 | ## Install dependencies 118 | In order to use pipenv and avoid confusions we are going to generate a Pipfile with the existing requirements and then remove all "requirements.txt" type of files. 119 | 120 | 1. Install the main dependencies: `$ pipenv install -r requirements/production.txt` 121 | 2. Open `requirements/local.txt` and delete the first line: `-r base.txt`. This will prevent the duplication of all base dependencies under `[dev-packages]` in the Pipfile. 122 | 3. Install dev dependencies: `$ pipenv install -r requirements/local.txt --dev` 123 | 4. Delete the following files and folder: 124 | - `requirements.txt` from the root folder of the project. 125 | - `requirements/base.txt`, `requirements/local.txt`, `requirements/production.txt` and the `requirements` folder. 126 | 127 | NOTES: 128 | - Remember, when you install a new dependency, check if it will be needed at production or not. In the case that it won't be needed, add the flag `--dev` at the end of the installation command. 129 | - In the Pipfile, try to avoid the use of `"*"` for the library versions. For each library set the used version in the project. This is to avoid future compatibility errors. 130 | - At `pipenv` to run a command, you have two options: 131 | 1. You can run `$ pipenv shell`, which creates a shell with the virtual environment activated, and in there, you can run any command without prefixes. For example, the command that runs the server should be just `$ python manage.py runserver` 132 | 2. If you haven't entered to the shell, you can write `pipenv run` before any command, for example, the command that runs the server should be `$ pipenv run python manage.py runserver` 133 | 134 | ## Create psql database 135 | `$ createdb ` 136 | 137 | For example `$ createdb cookiecutter_starter-db-dev` 138 | 139 | ## Load .env file for local configurations 140 | 1. At `config/settings/base.py` set `DJANGO_READ_DOT_ENV_FILE` to load the configurations from `.env` file. 141 | ```python 142 | # config/settings/base.py 143 | 144 | # Before 145 | READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) 146 | 147 | # After 148 | READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=True) 149 | ``` 150 | 151 | 2. Create `.env` file at root folder and `.env.example` to track the local configuration needed. 152 | ```yml 153 | # .env.example 154 | 155 | DJANGO_DEBUG=on 156 | DATABASE_URL=postgres://:@localhost:5432/ 157 | ``` 158 | 159 | For example, let's say you have: 160 | - A created db named `cookiecutter_starter-db-dev`, and a user `postgres` with password `postgres` 161 | - Then the value for `DATABASE_URL` would be: `DATABASE_URL=postgres://postgres:postgres@localhost:5432/cookiecutter_starter-db-dev` 162 | 163 | IMPORTANT SUGGESTION: 164 | - For critical or sensitive information/credentials use an environment variable in `.env` file and add it also to `.env.example` for consistency. Also, set the definition of the env var in the config file without the `default` parameter. 165 | 166 | ### Important Note 167 | For the next commands, we will assume that you are in the root folder, and you have already entered to the sell with `$ pipenv shell` 168 | 169 | ## Migrate 170 | `$ python manage.py migrate` 171 | 172 | ## Run test 173 | `$ pytest` 174 | 175 | ## Run test and get coverage percentage 176 | `$ coverage run -m pytest && pipenv run coverage report -m` 177 | 178 | NOTE: 179 | - The command must run without exceptions because it's mandatory for future configurations. 180 | - Don't matter if you write tests following the [unittest](https://docs.python.org/3/library/unittest.html) or [pytest](https://docs.pytest.org/) approach since `$ pytest` command will be able to run both of them. 181 | 182 | ## Install Mailhog 183 | `$ brew install mailhog` 184 | 185 | NOTE: If you prefer other options to install *Mailhog*, you can take a look at the [Mailhog installation guideline](https://github.com/mailhog/MailHog#installation). 186 | 187 | Start the server running `$ mailhog` 188 | 189 | Finally, if you go to `http://localhost:8025/`, your mail server is running. 190 | 191 | ## Code Style 192 | ### [flake8](https://flake8.pycqa.org/) and [flake8-isort](https://github.com/gforcada/flake8-isort) 193 | *NOTE: By default this package is already installed.* 194 | 195 | 1. `$ pipenv install flake8 flake8-isort --dev` 196 | 2. Add custom configuration at `setup.cfg` file according to the Rootstrap's standards. 197 | - **extend-ignore**: exclude flake8 warnings to avoid black conflicts. [See more.](https://black.readthedocs.io/en/stable/compatible_configs.html#id2) 198 | ```yml 199 | # setup.cfg 200 | 201 | [flake8] 202 | max-line-length = 120 203 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv/* 204 | extend-ignore = E203, W503 205 | ``` 206 | 3. Check running `$ flake8 .` 207 | - This command list all the issues found. 208 | - A useful command could be `$ isort .` which resolves all the issues related to import the styles. Also, you can add the flag `--interactive` at the end of the command to resolve or ignore one issue by one. 209 | 210 | ### [black](https://black.readthedocs.io/) 211 | *NOTE: By default this package is already installed.* 212 | 213 | 1. `$ pipenv install black --dev` 214 | 2. Add custom configuration creating a `pyproject.toml` file in the root folder according to the Rootstrap's standards and adding the next configuration. 215 | - *black* does not support configurations at `setup.cfg` file and there isn't a plan to do it in the future. [See more.](https://github.com/psf/black/issues/683) 216 | ```yml 217 | # pyproject.toml 218 | 219 | [tool.black] 220 | skip_string_normalization = false 221 | line-length = 120 222 | exclude = '.*\/(migrations|settings)\/.*' 223 | ``` 224 | 3. Check running `$ black . --check` 225 | - This command list all the issues found. 226 | - A useful command could be `$ black .` which resolves all the issues. Also, you can run `$ black . --diff` to watch the proposed changes. 227 | 228 | ### If you want to have this in a new Github repository 229 | If you want to have this project in a new Github repository under the rootstrap organization, then follow these steps: 230 | 1. Go to Github. 231 | 2. Click on create new repository. 232 | 3. Under Owner, select rootstrap. 233 | 4. Write the name for the respository equal to the name of the root folder of the project created with DjangoCookieCutter. 234 | 5. Copy the `git clone with ssh value` from the project. 235 | 6. Go to the root folder project in your terminal. 236 | 7. Run `$ git init` 237 | 8. Run `$ git remote add origin ` 238 | 9. If your current branch hasn't the `main` branch, then run `$ git checkout -b main` 239 | 10. Run `$ git add .` 240 | 11. Run `$ git commit -m "First commit"`. You can change the `"First commit"` message with whatever you think is correct. 241 | 12. Run `$ git push origin main` 242 | 13. If you had to run `$ git checkout -b main` then now run `$ git branch -M main` 243 | 244 | ### [pre-commit](https://pre-commit.com/) 245 | *NOTE: By default this package is already installed.* 246 | 247 | This is optional and is a local configuration. A sample `pre-commit-config` hook already exists in the generated project as default. 248 | 1. `$ pipenv install pre-commit --dev` 249 | 2. `$ pre-commit install` # A git repo is required for pre-commit to install 250 | 251 | This will run flake8, black and isort locally before creating a commit. This can avoid having to wait for the CI to fail to remember that we missed some styling issues. You can ignore the pre commit hook by adding `-n` when creating a commit (e.g. `git commit -m 'ignore pre-commit' -n`). 252 | 253 | You can check the `.pre-commit-config.yaml` file for more details. 254 | 255 | Note: the first commit after running step 2 will take a while but after that it will be pretty fast. 256 | 257 | ### [mypy](https://mypy.readthedocs.io/en/stable/) 258 | *NOTE: By default, this package is already installed but it is NOT a stage added to pre-commit or Github Actions.* 259 | First, take a look at [typing hints](https://github.com/rootstrap/tech-guides/tree/master/python#typing-hints) document in the Python section of the tech guides. 260 | Cookiecutter already configures Mypy in `setup.cfg` file, if you need to change any of these configurations, please check the `[mypy]` section in this file. 261 | Adding this typing hint check library to pre-commit or GitHub Workflow Actions is optional, it depends on your project scope. 262 | If you add Mypy to pre-commit it will check the typing in the files you change on your commit, to add Mypy to pre-commit you will need to add something like the following: 263 |
264 | .pre-commit-config.yaml (Click ▶ to display the file) 265 | 266 | ```yaml 267 | # .pre-commit-config.yaml 268 | ... 269 | 270 | - repo: https://github.com/pre-commit/mirrors-mypy 271 | rev: v0.942 272 | hooks: 273 | - id: mypy 274 | additional_dependencies: [django-stubs] 275 | 276 | ... 277 | ``` 278 | You may need more configuration in `additional_dependencies` depending on your project libraries because these pre-commit stages are running in an independent environment. 279 | 280 |
281 | 282 | If you want to check the Github Wokflow configuration please take a look at this [section](#github-workflow) 283 | 284 | A useful command could be `$ mypy .`, which shows you all the typing issues you have in all your Python files. 285 | 286 | ### Single-quotes 287 | > *Here at Rootstrap we prefer to use single-quoted strings over double quotes. For docstrings we use double quotes since the chance of writing a ' is higher in the documentation string of a class or a method.* 288 | > 289 | > [Rootstrap Guides/Python/String Quotes](https://github.com/rootstrap/tech-guides/tree/master/python#string-quotes) 290 | 291 | To convert the existing double quotes to single ones, follow these steps: 292 | 1. In your IDE, search by the regex `/(? 314 | .codeclimate.yml. (Click ▶ to display the file) 315 | 316 | ```yml 317 | # .codeclimate.yml 318 | version: "2" 319 | 320 | checks: 321 | argument-count: 322 | config: 323 | threshold: 5 # Allow at most 5 arguments in a function 324 | method-count: 325 | config: 326 | threshold: 25 # Allow at most 25 functions in a class 327 | 328 | exclude_patterns: 329 | - "**/migrations/**" 330 | - "**/settings/**" 331 | - "docs/" 332 | - "**/*.env" 333 | - "build/" 334 | - "dist/" 335 | - "**__pycache__**" 336 | 337 | plugins: 338 | pep8: 339 | enabled: true 340 | duplication: 341 | enabled: true 342 | exclude_patterns: 343 | - "**/tests/**" # Don't check duplication on tests 344 | ``` 345 | 346 | 347 | ### GitHub Workflow 348 | 349 | 1. Create the path `.github/workflows` and inner it the file `python-app.yml` (or any file name you want as long it is in this folder) with the following content. 350 | 2. Create a project secret to use it at the config file. [guide](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository). 351 | - Name: `CC_TEST_REPORTER_ID` 352 | - Value: `Test Coverage ID` 353 | 3. Add badge to readme file. [guide](https://docs.github.com/en/actions/managing-workflow-runs/adding-a-workflow-status-badge#using-the-event-parameter) 354 | - The workflow name is the value of the first configuration `name`, in the example above is `Python application`. 355 | 356 | ```markdown 357 | # Template: 358 | .. image:: https://github.com///workflows//badge.svg 359 | 360 | # Example: 361 | .. image:: https://github.com/afmicc/cookiecutter_starter/workflows/Python%20application/badge.svg 362 | :alt: Tests status 363 | ``` 364 | 365 |
366 | .github/workflows/python-app.yml. (Click ▶ to display the file) 367 | 368 | ```yml 369 | # .github/workflows/python-app.yml 370 | 371 | name: Python application 372 | 373 | on: [push, pull_request] 374 | 375 | jobs: 376 | build: 377 | runs-on: ubuntu-latest 378 | services: 379 | db: 380 | image: postgres:11.6-alpine 381 | env: 382 | POSTGRES_PASSWORD: postgres 383 | POSTGRES_USER: postgres 384 | ports: 385 | - "5432:5432" 386 | env: 387 | DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres 388 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 389 | 390 | steps: 391 | - uses: actions/checkout@v2 392 | 393 | - name: Set up Python 3.8 394 | uses: actions/setup-python@v2 395 | with: 396 | python-version: 3.8 397 | 398 | - name: Installing resoruces 399 | run: | 400 | pip install --upgrade pip==20.0.2 401 | pip install pipenv 402 | 403 | - name: Cache pipenv 404 | uses: actions/cache@v2 405 | id: pipenv-cache 406 | with: 407 | path: ~/.local/share/virtualenvs 408 | key: ${{ runner.os }}-pipenv-cache-${{ hashFiles('Pipfile.lock') }} 409 | restore-keys: | 410 | ${{ runner.os }}-pipenv-cache 411 | 412 | - name: Installing requirements 413 | if: steps.pipenv-cache.outputs.cache-hit != 'true' 414 | run: | 415 | pipenv install --dev 416 | 417 | - name: Checking PEP8 code style 418 | run: | 419 | pipenv run flake8 --count 420 | 421 | - name: Checking Black code formatter 422 | run: | 423 | pipenv run black . --check 424 | 425 | - name: Check typing 426 | run: | 427 | pipenv run mypy . 428 | 429 | - name: Running tests 430 | run: | 431 | pipenv run coverage run -m pytest --ds=config.settings.test 432 | 433 | - name: Checking coverage 434 | run: | 435 | pipenv run coverage report --fail-under=90 -m 436 | pipenv run coverage xml 437 | 438 | - name: Setup Code Climate test-reporter 439 | run: | 440 | curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter 441 | chmod +x ./cc-test-reporter 442 | ./cc-test-reporter before-build 443 | ./cc-test-reporter after-build --coverage-input-type coverage.py --exit-code $? 444 | ``` 445 | 446 |
447 | 448 | ## Adding a new app 449 | 450 | ### Important Note 451 | For the next commands, we will also assume that you are in the root folder, and you have already entered to the sell with `$ pipenv shell` 452 | 453 | 1. To create a new app into the project, we have to run the command `$ python manage.py startapp ` 454 | - **new_app_name**: is the name of the new app and it has to be written in plural. For example: targets 455 | 2. Since that command creates the new app at the root directory, you have to move it to `` directory, where there are other existing apps like `users`. 456 | - **project_name**: is the name that you define in the first step of the project creation. For example: cookiecutter_starter 457 | 3. Other change is needed, you have to edit the `apps.py` file of the new app and replace the current `name` field of the config class from only `` to `.` 458 | ```python 459 | # cookiecutter_starter/targets/apps.py 460 | 461 | # Before 462 | 463 | class TargetsConfig(AppConfig): 464 | name = 'targets' 465 | 466 | # After 467 | 468 | class TargetsConfig(AppConfig): 469 | name = 'cookiecutter_starter.targets' 470 | ``` 471 | 4. Finally, you need to add the created app to the `INSTALLED_APPS` configuration. We need to go to the file `config/settings/base.py` and find the `LOCAL_APPS` variable where we should add our recently created app following this pattern: `..apps.`. As we can see in the following example, the `LOCAL_APPS` is concatenated to other required apps to create the recognized `INSTALLED_APPS` variable, this is just a splitting that applies Cookiecutter to improve the organization of the app in the config files. 472 | ```python 473 | # config/settings/base.py 474 | ... 475 | 476 | LOCAL_APPS = [ 477 | 'cookiecutter_starter.users.apps.UsersConfig', 478 | # Your stuff: custom apps go here 479 | 'cookiecutter_starter.targets.apps.TargetsConfig', 480 | ] 481 | 482 | INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 483 | 484 | ... 485 | ``` 486 | 487 | ### Solution Structure 488 | 489 | The internal organization of the structure of an app could depend on the different requirements and circumstances that you have to solve. At Rootstrap, we provide a recommended structure which you could follow whenever you can. 490 | 491 |
492 | Structure. (Click ▶ to display the file) 493 | 494 | ``` 495 | 496 | ├── config 497 | │ ├── settings 498 | │ │ ├── ... 499 | │ │ ├── base.py - Here is the base.py file that we mentioned before 500 | │ │ ├── local.py 501 | │ │ ├── production.py 502 | │ │ └── test.py 503 | │ ├── urls.py 504 | │ └── ... 505 | ├── - Where you place new apps 506 | │ ├── templates 507 | │ │ ├── 508 | │ │ │ └── _form.html 509 | │ │ ├── users 510 | │ │ │ └── ... 511 | │ │ ├── ... 512 | │ │ ├── 403.html 513 | │ │ ├── 404.html 514 | │ │ ├── 500.html 515 | │ │ └── base.html 516 | │ ├── 517 | │ │ ├── api 518 | │ │ │ ├── serializers.py 519 | │ │ │ └── views.py 520 | │ │ ├── migrations 521 | │ │ │ └── ... 522 | │ │ ├── tests 523 | │ │ │ ├── __init__.py 524 | │ │ │ ├── factories.py 525 | │ │ │ ├── test_forms.py 526 | │ │ │ ├── test_models.py 527 | │ │ │ ├── test_urls.py 528 | │ │ │ └── test_views.py 529 | │ │ ├── utils 530 | │ │ │ └── ... 531 | │ │ ├── __init__.py 532 | │ │ ├── admin.py 533 | │ │ ├── apps.py - Here is the apps.py file that we mentioned before 534 | │ │ ├── forms.py 535 | │ │ ├── models.py 536 | │ │ ├── urls.py 537 | │ │ └── views.py 538 | │ ├── users 539 | │ │ └── ... 540 | │ └── ... 541 | ├── ... 542 | └── manage.py 543 | ``` 544 | 545 |
546 | 547 | ## Drf-spectacular 548 | 549 | This [library](https://drf-spectacular.readthedocs.io/en/latest/) is a sane and flexible [OpenAPI 3.0](https://spec.openapis.org/oas/v3.0.3) schema generation for Django REST framework. 550 | It is already installed and configured by default in the creation of a Cookiecutter project in Django. 551 | The project has 3 important goals: 552 | * Extract as much schema information from DRF as possible. 553 | * Provide flexibility to make the schema usable in the real world (not only toy examples). It is relevant to mention here that a commonly used tool at RootStrap is [Apiary](https://apiary.io/). This tool supports [OpenAPI schemas](https://help.apiary.io/swagger/). 554 | * Generate a schema that works well with the most popular client generators. 555 | 556 | You will see something like this in your app urls.py: 557 | 558 | ```python 559 | # config/urls.py 560 | from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView 561 | 562 | ... 563 | 564 | urlpatterns = [ 565 | ... 566 | # YOUR PATTERNS 567 | path('api/schema/', SpectacularAPIView.as_view(), name='api-schema'), 568 | # This will download a .yaml file with your api schema 569 | # Optional UI: 570 | path('api/docs/', SpectacularSwaggerView.as_view(url_name='api-schema'), name='api-docs'), 571 | # This will enable an UI to show your api schema in HTML. 572 | ] 573 | ``` 574 | 575 | The UI views will be like the following images: 576 |
577 | Images (Click ▶ to display the images) 578 | Generally it will be like this: 579 | 580 | ![DRF-spectacular UI](images/drf-spectacular-1.png) 581 | 582 | In every endpoint, you will see a description of the response and the parameters allowed. Even you can use the endpoint from this web interface. 583 | 584 | ![DRF-spectacular detail](images/drf-spectacular-2.png) 585 | 586 |
587 | 588 | This library is also extra customizable. You could use the `@extend_schema` decorator to customize APIView, Viewsets, function-based views, and @action. This decorator will allow you to add additional parameters or customize the existing ones (description, types), override request/response serializers, and more! 589 | 590 |
591 | @extend_schema decorator example (Click ▶ to display example) 592 | 593 | Example extracted from drf-spectacular documentation. 594 | 595 | ```python 596 | from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiExample 597 | from drf_spectacular.types import OpenApiTypes 598 | 599 | class AlbumViewset(viewset.ModelViewset): 600 | serializer_class = AlbumSerializer 601 | 602 | @extend_schema( 603 | request=AlbumCreationSerializer, 604 | responses={201: AlbumSerializer}, 605 | ) 606 | def create(self, request): 607 | # your non-standard behaviour 608 | return super().create(request) 609 | 610 | @extend_schema( 611 | # extra parameters added to the schema 612 | parameters=[ 613 | OpenApiParameter(name='artist', description='Filter by artist', required=False, type=str), 614 | OpenApiParameter( 615 | name='release', 616 | type=OpenApiTypes.DATE, 617 | location=OpenApiParameter.QUERY, 618 | description='Filter by release date', 619 | examples=[ 620 | OpenApiExample( 621 | 'Example 1', 622 | summary='short optional summary', 623 | description='longer description', 624 | value='1993-08-23' 625 | ), 626 | ... 627 | ], 628 | ), 629 | ], 630 | # override default docstring extraction 631 | description='More descriptive text', 632 | # provide Authentication class that deviates from the views default 633 | auth=None, 634 | # change the auto-generated operation name 635 | operation_id=None, 636 | # or even completely override what AutoSchema would generate. Provide raw Open API spec as Dict. 637 | operation=None, 638 | # attach request/response examples to the operation. 639 | examples=[ 640 | OpenApiExample( 641 | 'Example 1', 642 | description='longer description', 643 | value=... 644 | ), 645 | ... 646 | ], 647 | ) 648 | def list(self, request): 649 | # your non-standard behaviour 650 | return super().list(request) 651 | ``` 652 | 653 |
654 | 655 | Please check the [drf-spectacular](https://drf-spectacular.readthedocs.io/en/latest/) documentation to find out more about it. 656 | 657 | ## Final 658 | 659 | Congratulations! Your project is fully configured 💪 660 | 661 | Here the example project: [afmicc/cookiecutter_starter](https://github.com/afmicc/cookiecutter_starter) 662 | -------------------------------------------------------------------------------- /ruby/rails.md: -------------------------------------------------------------------------------- 1 | # Prelude 2 | 3 | > Role models are important.
4 | > -- Officer Alex J. Murphy / RoboCop 5 | 6 | The goal of this guide is to present a set of best practices and style 7 | prescriptions for Ruby on Rails 4/5 development. 8 | Some of the advice here is applicable only to Rails 4.0+. 9 | 10 | # The Rails Style Guide 11 | 12 | This Rails style guide recommends best practices so that real-world Rails 13 | programmers can write code that can be maintained by other real-world Rails 14 | programmers. A style guide that reflects real-world usage gets used, and a 15 | style guide that holds to an ideal that has been rejected by the people it 16 | is supposed to help risks not getting used at all – no matter how good 17 | it is. 18 | 19 | 20 | ## Table of Contents 21 | 22 | * [Configuration](#configuration) 23 | * [Routing](#routing) 24 | * [Controllers](#controllers) 25 | * [Rendering](#rendering) 26 | * [Models](#models) 27 | * [ActiveRecord](#activerecord) 28 | * [ActiveRecord Queries](#activerecord-queries) 29 | * [Migrations](#migrations) 30 | * [Views](#views) 31 | * [Internationalization](#internationalization) 32 | * [Assets](#assets) 33 | * [Mailers](#mailers) 34 | * [Active Support Core Extensions](#active-support-core-extensions) 35 | * [Time](#time) 36 | * [Bundler](#bundler) 37 | * [Managing processes](#managing-processes) 38 | * [Logging](#logging) 39 | 40 | ## Configuration 41 | 42 | * 43 | Put custom initialization code in `config/initializers`. The code in 44 | initializers executes on application startup. 45 | [[link](#config-initializers)] 46 | 47 | * 48 | Keep initialization code for each gem in a separate file with the same name 49 | as the gem, for example `carrierwave.rb`, `active_admin.rb`, etc. 50 | [[link](#gem-initializers)] 51 | 52 | * 53 | Adjust accordingly the settings for development, test and production 54 | environment (in the corresponding files under `config/environments/`) 55 | [[link](#dev-test-prod-configs)] 56 | 57 | * Mark additional assets for precompilation (if any): 58 | 59 | ```Ruby 60 | # config/environments/production.rb 61 | # Precompile additional assets (application.js, application.css, 62 | #and all non-JS/CSS are already added) 63 | config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js ) 64 | ``` 65 | 66 | * 67 | Keep configuration that's applicable to all environments in the 68 | `config/application.rb` file. 69 | [[link](#app-config)] 70 | 71 | * 72 | Create an additional `staging` environment that closely resembles the 73 | `production` one. 74 | [[link](#staging-like-prod)] 75 | 76 | * 77 | Keep any additional configuration in YAML files under the `config/` directory. 78 | [[link](#yaml-config)] 79 | 80 | Since Rails 4.2 YAML configuration files can be easily loaded with the new `config_for` method: 81 | 82 | ```Ruby 83 | Rails::Application.config_for(:yaml_file) 84 | ``` 85 | 86 | ## Routing 87 | * 88 | Prefer `resources` over custom routes. 89 | [[link](#resource-routing)] 90 | 91 | ```Ruby 92 | # bad 93 | get 'topics/:id', to: 'topics#show' 94 | 95 | # good 96 | resources :topics, only: :show 97 | ``` 98 | 99 | * 100 | When you need to add more actions to a RESTful resource (do you really need 101 | them at all?) use `member` and `collection` routes. 102 | [[link](#member-collection-routes)] 103 | 104 | ```Ruby 105 | # bad 106 | get 'subscriptions/:id/unsubscribe' 107 | resources :subscriptions 108 | 109 | # good 110 | resources :subscriptions do 111 | get 'unsubscribe', on: :member 112 | end 113 | 114 | # bad 115 | get 'photos/search' 116 | resources :photos 117 | 118 | # good 119 | resources :photos do 120 | get 'search', on: :collection 121 | end 122 | ``` 123 | 124 | * 125 | If you need to define multiple `member/collection` routes use the 126 | alternative block syntax. 127 | [[link](#many-member-collection-routes)] 128 | 129 | ```Ruby 130 | resources :subscriptions do 131 | member do 132 | get 'unsubscribe' 133 | # more routes 134 | end 135 | end 136 | 137 | resources :photos do 138 | collection do 139 | get 'search' 140 | # more routes 141 | end 142 | end 143 | ``` 144 | 145 | * 146 | Use nested routes to express better the relationship between ActiveRecord 147 | models. 148 | [[link](#nested-routes)] 149 | 150 | ```Ruby 151 | class Post < ActiveRecord::Base 152 | has_many :comments 153 | end 154 | 155 | class Comments < ActiveRecord::Base 156 | belongs_to :post 157 | end 158 | 159 | # routes.rb 160 | resources :posts do 161 | resources :comments 162 | end 163 | ``` 164 | 165 | * 166 | 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`. 167 | 168 | ```Ruby 169 | resources :posts, shallow: true do 170 | resources :comments do 171 | resources :versions 172 | end 173 | end 174 | ``` 175 | 176 | * 177 | Use namespaced routes to group related actions. 178 | [[link](#namespaced-routes)] 179 | 180 | ```Ruby 181 | namespace :admin do 182 | # Directs /admin/products/* to Admin::ProductsController 183 | # (app/controllers/admin/products_controller.rb) 184 | resources :products 185 | end 186 | ``` 187 | 188 | * 189 | Never use the legacy wild controller route. This route will make all actions 190 | in every controller accessible via GET requests. 191 | [[link](#no-wild-routes)] 192 | 193 | ```Ruby 194 | # very bad 195 | match ':controller(/:action(/:id(.:format)))' 196 | ``` 197 | 198 | * 199 | 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. 200 | [[link](#no-match-routes)] 201 | 202 | * 203 | Don't create routes that are not being used. Use `:only` and `:except` to generate only the routes you actually need. 204 | [[link](#non-existing-routes)] 205 | 206 | ## Controllers 207 | 208 | * 209 | Keep the controllers skinny - they should only retrieve data for the view 210 | layer and shouldn't contain any business logic (all the business logic 211 | should naturally reside in the model). 212 | [[link](#skinny-controllers)] 213 | 214 | * 215 | Each controller action should (ideally) invoke only one method other than an 216 | initial find or new. 217 | [[link](#one-method)] 218 | 219 | * 220 | Share no more than two instance variables between a controller and a view. 221 | [[link](#shared-instance-variables)] 222 | 223 | 224 | ### Rendering 225 | 226 | * 227 | Prefer using a template over inline rendering. 228 | [[link](#inline-rendering)] 229 | 230 | ```Ruby 231 | # very bad 232 | class ProductsController < ApplicationController 233 | def index 234 | render inline: "<% products.each do |p| %>

<%= p.name %>

<% end %>", type: :erb 235 | end 236 | end 237 | 238 | # good 239 | ## app/views/products/index.html.erb 240 | <%= render partial: 'product', collection: products %> 241 | 242 | ## app/views/products/_product.html.erb 243 |

<%= product.name %>

244 |

<%= product.price %>

245 | 246 | ## app/controllers/foo_controller.rb 247 | class ProductsController < ApplicationController 248 | def index 249 | render :index 250 | end 251 | end 252 | ``` 253 | 254 | * 255 | Prefer `render plain:` over `render text:`. 256 | [[link](#plain-text-rendering)] 257 | 258 | ```Ruby 259 | # bad - sets MIME type to `text/html` 260 | ... 261 | render text: 'Ruby!' 262 | ... 263 | 264 | # bad - requires explicit MIME type declaration 265 | ... 266 | render text: 'Ruby!', content_type: 'text/plain' 267 | ... 268 | 269 | # good - short and precise 270 | ... 271 | render plain: 'Ruby!' 272 | ... 273 | ``` 274 | 275 | * 276 | 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. 277 | [[link](#http-status-code-symbols)] 278 | 279 | ```Ruby 280 | # bad 281 | ... 282 | render status: 500 283 | ... 284 | 285 | # good 286 | ... 287 | render status: :forbidden 288 | ... 289 | ``` 290 | 291 | ## Models 292 | 293 | * 294 | Introduce non-ActiveRecord model classes freely. 295 | [[link](#model-classes)] 296 | 297 | * 298 | Name the models with meaningful (but short) names without abbreviations. 299 | [[link](#meaningful-model-names)] 300 | 301 | * 302 | If you need model objects that support ActiveRecord behavior (like validation) 303 | without the ActiveRecord database functionality use the 304 | [ActiveAttr](https://github.com/cgriego/active_attr) gem. 305 | [[link](#activeattr-gem)] 306 | 307 | ```Ruby 308 | class Message 309 | include ActiveAttr::Model 310 | 311 | attribute :name 312 | attribute :email 313 | attribute :content 314 | attribute :priority 315 | 316 | attr_accessible :name, :email, :content 317 | 318 | validates :name, presence: true 319 | validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i } 320 | validates :content, length: { maximum: 500 } 321 | end 322 | ``` 323 | 324 | For a more complete example refer to the 325 | [RailsCast on the subject](http://railscasts.com/episodes/326-activeattr). 326 | 327 | * 328 | Unless they have some meaning in the business domain, don't put methods in 329 | your model that just format your data (like code generating HTML). These 330 | methods are most likely going to be called from the view layer only, so their 331 | place is in decorators. Keep your models for business logic and data-persistence 332 | only. 333 | [[link](#model-business-logic)] 334 | 335 | ### ActiveRecord 336 | 337 | * 338 | Avoid altering ActiveRecord defaults (table names, primary key, etc) unless 339 | you have a very good reason (like a database that's not under your control). 340 | [[link](#keep-ar-defaults)] 341 | 342 | ```Ruby 343 | # bad - don't do this if you can modify the schema 344 | class Transaction < ActiveRecord::Base 345 | self.table_name = 'order' 346 | ... 347 | end 348 | ``` 349 | 350 | * 351 | Group macro-style methods (`has_many`, `validates`, etc) in the beginning of 352 | the class definition. 353 | [[link](#macro-style-methods)] 354 | 355 | ```Ruby 356 | class User < ActiveRecord::Base 357 | # keep the default scope first (if any) 358 | default_scope { where(active: true) } 359 | 360 | # constants come up next 361 | COLORS = %w(red green blue) 362 | 363 | # afterwards we put attr related macros 364 | attr_accessor :formatted_date_of_birth 365 | 366 | attr_accessible :login, :first_name, :last_name, :email, :password 367 | 368 | # Rails4+ enums after attr macros, prefer the hash syntax 369 | enum gender: { female: 0, male: 1 } 370 | 371 | # followed by association macros 372 | belongs_to :country 373 | 374 | has_many :authentications, dependent: :destroy 375 | 376 | # and validation macros 377 | validates :email, presence: true 378 | validates :username, presence: true 379 | validates :username, uniqueness: { case_sensitive: false } 380 | validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ } 381 | validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true } 382 | 383 | # next we have callbacks 384 | before_save :cook 385 | before_save :update_username_lower 386 | 387 | # other macros (like devise's) should be placed after the callbacks 388 | 389 | ... 390 | end 391 | ``` 392 | 393 | * 394 | Prefer `has_many :through` to `has_and_belongs_to_many`. Using `has_many 395 | :through` allows additional attributes and validations on the join model. 396 | [[link](#has-many-through)] 397 | 398 | ```Ruby 399 | # not so good - using has_and_belongs_to_many 400 | class User < ActiveRecord::Base 401 | has_and_belongs_to_many :groups 402 | end 403 | 404 | class Group < ActiveRecord::Base 405 | has_and_belongs_to_many :users 406 | end 407 | 408 | # preferred way - using has_many :through 409 | class User < ActiveRecord::Base 410 | has_many :memberships 411 | has_many :groups, through: :memberships 412 | end 413 | 414 | class Membership < ActiveRecord::Base 415 | belongs_to :user 416 | belongs_to :group 417 | end 418 | 419 | class Group < ActiveRecord::Base 420 | has_many :memberships 421 | has_many :users, through: :memberships 422 | end 423 | ``` 424 | 425 | * 426 | Prefer `self[:attribute]` over `read_attribute(:attribute)`. 427 | [[link](#read-attribute)] 428 | 429 | ```Ruby 430 | # bad 431 | def amount 432 | read_attribute(:amount) * 100 433 | end 434 | 435 | # good 436 | def amount 437 | self[:amount] * 100 438 | end 439 | ``` 440 | 441 | * 442 | Prefer `self[:attribute] = value` over `write_attribute(:attribute, value)`. 443 | [[link](#write-attribute)] 444 | 445 | ```Ruby 446 | # bad 447 | def amount 448 | write_attribute(:amount, 100) 449 | end 450 | 451 | # good 452 | def amount 453 | self[:amount] = 100 454 | end 455 | ``` 456 | 457 | * 458 | Always use the new ["sexy" 459 | validations](http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/). 460 | [[link](#sexy-validations)] 461 | 462 | ```Ruby 463 | # bad 464 | validates_presence_of :email 465 | validates_length_of :email, maximum: 100 466 | 467 | # good 468 | validates :email, presence: true, length: { maximum: 100 } 469 | ``` 470 | 471 | * 472 | When a custom validation is used more than once or the validation is some 473 | regular expression mapping, create a custom validator file. 474 | [[link](#custom-validator-file)] 475 | 476 | ```Ruby 477 | # bad 478 | class Person 479 | validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i } 480 | end 481 | 482 | # good 483 | class EmailValidator < ActiveModel::EachValidator 484 | def validate_each(record, attribute, value) 485 | record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 486 | end 487 | end 488 | 489 | class Person 490 | validates :email, email: true 491 | end 492 | ``` 493 | 494 | * 495 | Keep custom validators under `app/validators`. 496 | [[link](#app-validators)] 497 | 498 | * 499 | Consider extracting custom validators to a shared gem if you're maintaining 500 | several related apps or the validators are generic enough. 501 | [[link](#custom-validators-gem)] 502 | 503 | * 504 | Use named scopes freely. 505 | [[link](#named-scopes)] 506 | 507 | ```Ruby 508 | class User < ActiveRecord::Base 509 | scope :active, -> { where(active: true) } 510 | scope :inactive, -> { where(active: false) } 511 | 512 | scope :with_orders, -> { joins(:orders).select('distinct(users.id)') } 513 | end 514 | ``` 515 | 516 | * 517 | When a named scope defined with a lambda and parameters becomes too 518 | complicated, it is preferable to make a class method instead which serves the 519 | same purpose of the named scope and returns an `ActiveRecord::Relation` 520 | object. Arguably you can define even simpler scopes like this. 521 | [[link](#named-scope-class)] 522 | 523 | ```Ruby 524 | class User < ActiveRecord::Base 525 | def self.with_orders 526 | joins(:orders).select('distinct(users.id)') 527 | end 528 | end 529 | ``` 530 | 531 | * 532 | Beware of the behavior of the 533 | [following](http://guides.rubyonrails.org/active_record_validations.html#skipping-validations) 534 | methods. They do not run the model validations and 535 | could easily corrupt the model state. 536 | [[link](#beware-skip-model-validations)] 537 | 538 | ```Ruby 539 | # bad 540 | Article.first.decrement!(:view_count) 541 | DiscussionBoard.decrement_counter(:post_count, 5) 542 | Article.first.increment!(:view_count) 543 | DiscussionBoard.increment_counter(:post_count, 5) 544 | person.toggle :active 545 | product.touch 546 | Billing.update_all("category = 'authorized', author = 'David'") 547 | user.update_attribute(:website, 'example.com') 548 | user.update_columns(last_request_at: Time.current) 549 | Post.update_counters 5, comment_count: -1, action_count: 1 550 | 551 | # good 552 | user.update_attributes(website: 'example.com') 553 | ``` 554 | 555 | * 556 | Use user-friendly URLs. Show some descriptive attribute of the model in the URL 557 | rather than its `id`. There is more than one way to achieve this: 558 | [[link](#user-friendly-urls)] 559 | 560 | * Override the `to_param` method of the model. This method is used by Rails 561 | for constructing a URL to the object. The default implementation returns 562 | the `id` of the record as a String. It could be overridden to include another 563 | human-readable attribute. 564 | 565 | ```Ruby 566 | class Person 567 | def to_param 568 | "#{id} #{name}".parameterize 569 | end 570 | end 571 | ``` 572 | 573 | In order to convert this to a URL-friendly value, `parameterize` should be 574 | called on the string. The `id` of the object needs to be at the beginning so 575 | that it can be found by the `find` method of ActiveRecord. 576 | 577 | * Use the `friendly_id` gem. It allows creation of human-readable URLs by 578 | using some descriptive attribute of the model instead of its `id`. 579 | 580 | ```Ruby 581 | class Person 582 | extend FriendlyId 583 | friendly_id :name, use: :slugged 584 | end 585 | ``` 586 | 587 | Check the [gem documentation](https://github.com/norman/friendly_id) for more 588 | information about its usage. 589 | 590 | * 591 | Use `find_each` to iterate over a collection of AR objects. Looping through a 592 | collection of records from the database (using the `all` method, for example) 593 | is very inefficient since it will try to instantiate all the objects at once. 594 | In that case, batch processing methods allow you to work with the records in 595 | batches, thereby greatly reducing memory consumption. 596 | [[link](#find-each)] 597 | 598 | 599 | ```Ruby 600 | # bad 601 | Person.all.each do |person| 602 | person.do_awesome_stuff 603 | end 604 | 605 | Person.where('age > 21').each do |person| 606 | person.party_all_night! 607 | end 608 | 609 | # good 610 | Person.find_each do |person| 611 | person.do_awesome_stuff 612 | end 613 | 614 | Person.where('age > 21').find_each do |person| 615 | person.party_all_night! 616 | end 617 | ``` 618 | 619 | * 620 | Since [Rails creates callbacks for dependent 621 | associations](https://github.com/rails/rails/issues/3458), always call 622 | `before_destroy` callbacks that perform validation with `prepend: true`. 623 | [[link](#before_destroy)] 624 | 625 | ```Ruby 626 | # bad (roles will be deleted automatically even if super_admin? is true) 627 | has_many :roles, dependent: :destroy 628 | 629 | before_destroy :ensure_deletable 630 | 631 | def ensure_deletable 632 | fail "Cannot delete super admin." if super_admin? 633 | end 634 | 635 | # good 636 | has_many :roles, dependent: :destroy 637 | 638 | before_destroy :ensure_deletable, prepend: true 639 | 640 | def ensure_deletable 641 | fail "Cannot delete super admin." if super_admin? 642 | end 643 | ``` 644 | 645 | * 646 | Define the `dependent` option to the `has_many` and `has_one` associations. 647 | [[link](#has_many-has_one-dependent-option)] 648 | 649 | ```Ruby 650 | # bad 651 | class Post < ActiveRecord::Base 652 | has_many :comments 653 | end 654 | 655 | # good 656 | class Post < ActiveRecord::Base 657 | has_many :comments, dependent: :destroy 658 | end 659 | ``` 660 | 661 | * 662 | When persisting AR objects always use the exception raising bang! method or handle the method return value. 663 | This applies to `create`, `save`, `update`, `destroy`, `first_or_create` and `find_or_create_by`. 664 | [[link](#save-bang)] 665 | 666 | ```Ruby 667 | # bad 668 | user.create(name: 'Bruce') 669 | 670 | # bad 671 | user.save 672 | 673 | # good 674 | user.create!(name: 'Bruce') 675 | # or 676 | bruce = user.create(name: 'Bruce') 677 | if bruce.persisted? 678 | ... 679 | else 680 | ... 681 | end 682 | 683 | # good 684 | user.save! 685 | # or 686 | if user.save 687 | ... 688 | else 689 | ... 690 | end 691 | ``` 692 | 693 | ### ActiveRecord Queries 694 | 695 | * 696 | Avoid string interpolation in 697 | queries, as it will make your code susceptible to SQL injection 698 | attacks. 699 | [[link](#avoid-interpolation)] 700 | 701 | ```Ruby 702 | # bad - param will be interpolated unescaped 703 | Client.where("orders_count = #{params[:orders]}") 704 | 705 | # good - param will be properly escaped 706 | Client.where('orders_count = ?', params[:orders]) 707 | ``` 708 | 709 | * 710 | Consider using named placeholders instead of positional placeholders 711 | when you have more than 1 placeholder in your query. 712 | [[link](#named-placeholder)] 713 | 714 | ```Ruby 715 | # okish 716 | Client.where( 717 | 'created_at >= ? AND created_at <= ?', 718 | params[:start_date], params[:end_date] 719 | ) 720 | 721 | # good 722 | Client.where( 723 | 'created_at >= :start_date AND created_at <= :end_date', 724 | start_date: params[:start_date], end_date: params[:end_date] 725 | ) 726 | ``` 727 | 728 | * 729 | Favor the use of `find` over `where` 730 | when you need to retrieve a single record by id. 731 | [[link](#find)] 732 | 733 | ```Ruby 734 | # bad 735 | User.where(id: id).take 736 | 737 | # good 738 | User.find(id) 739 | ``` 740 | 741 | * 742 | Favor the use of `find_by` over `where` and `find_by_attribute` 743 | when you need to retrieve a single record by some attributes. 744 | [[link](#find_by)] 745 | 746 | ```Ruby 747 | # bad 748 | User.where(first_name: 'Bruce', last_name: 'Wayne').first 749 | 750 | # bad 751 | User.find_by_first_name_and_last_name('Bruce', 'Wayne') 752 | 753 | # good 754 | User.find_by(first_name: 'Bruce', last_name: 'Wayne') 755 | ``` 756 | 757 | * 758 | Favor the use of `where.not` over SQL. 759 | [[link](#where-not)] 760 | 761 | ```Ruby 762 | # bad 763 | User.where("id != ?", id) 764 | 765 | # good 766 | User.where.not(id: id) 767 | ``` 768 | * 769 | When specifying an explicit query in a method such as `find_by_sql`, use 770 | heredocs with `squish`. This allows you to legibly format the SQL with 771 | line breaks and indentations, while supporting syntax highlighting in many 772 | tools (including GitHub, Atom, and RubyMine). 773 | [[link](#squished-heredocs)] 774 | 775 | ```Ruby 776 | User.find_by_sql(<<-SQL.squish) 777 | SELECT 778 | users.id, accounts.plan 779 | FROM 780 | users 781 | INNER JOIN 782 | accounts 783 | ON 784 | accounts.user_id = users.id 785 | # further complexities... 786 | SQL 787 | ``` 788 | 789 | [`String#squish`](http://apidock.com/rails/String/squish) removes the indentation and newline characters so that your server 790 | log shows a fluid string of SQL rather than something like this: 791 | 792 | ``` 793 | SELECT\n users.id, accounts.plan\n FROM\n users\n INNER JOIN\n acounts\n ON\n accounts.user_id = users.id 794 | ``` 795 | 796 | ## Migrations 797 | 798 | * 799 | Keep the `schema.rb` (or `structure.sql`) under version control. 800 | [[link](#schema-version)] 801 | 802 | * 803 | Use `rake db:schema:load` instead of `rake db:migrate` to initialize an empty 804 | database. 805 | [[link](#db-schema-load)] 806 | 807 | * 808 | Enforce default values in the migrations themselves instead of in the 809 | application layer. 810 | [[link](#default-migration-values)] 811 | 812 | ```Ruby 813 | # bad - application enforced default value 814 | class Product < ActiveRecord::Base 815 | def amount 816 | self[:amount] || 0 817 | end 818 | end 819 | 820 | # good - database enforced 821 | class AddDefaultAmountToProducts < ActiveRecord::Migration 822 | def change 823 | change_column_default :products, :amount, 0 824 | end 825 | end 826 | ``` 827 | 828 | While enforcing table defaults only in Rails is suggested by many 829 | Rails developers, it's an extremely brittle approach that 830 | leaves your data vulnerable to many application bugs. And you'll 831 | have to consider the fact that most non-trivial apps share a 832 | database with other applications, so imposing data integrity from 833 | the Rails app is impossible. 834 | 835 | * 836 | Enforce foreign-key constraints. As of Rails 4.2, ActiveRecord 837 | supports foreign key constraints natively. 838 | [[link](#foreign-key-constraints)] 839 | 840 | * 841 | When writing constructive migrations (adding tables or columns), 842 | use the `change` method instead of `up` and `down` methods. 843 | [[link](#change-vs-up-down)] 844 | 845 | ```Ruby 846 | # the old way 847 | class AddNameToPeople < ActiveRecord::Migration 848 | def up 849 | add_column :people, :name, :string 850 | end 851 | 852 | def down 853 | remove_column :people, :name 854 | end 855 | end 856 | 857 | # the new preferred way 858 | class AddNameToPeople < ActiveRecord::Migration 859 | def change 860 | add_column :people, :name, :string 861 | end 862 | end 863 | ``` 864 | 865 | * 866 | If you have to use models in migrations, make sure you define them 867 | so that you don't end up with broken migrations in the future 868 | [[link](#define-model-class-migrations)] 869 | 870 | ```Ruby 871 | # db/migrate/.rb 872 | # frozen_string_literal: true 873 | 874 | # bad 875 | class ModifyDefaultStatusForProducts < ActiveRecord::Migration 876 | def change 877 | old_status = 'pending_manual_approval' 878 | new_status = 'pending_approval' 879 | 880 | reversible do |dir| 881 | dir.up do 882 | Product.where(status: old_status).update_all(status: new_status) 883 | change_column :products, :status, :string, default: new_status 884 | end 885 | 886 | dir.down do 887 | Product.where(status: new_status).update_all(status: old_status) 888 | change_column :products, :status, :string, default: old_status 889 | end 890 | end 891 | end 892 | end 893 | 894 | # good 895 | # Define `table_name` in a custom named class to make sure that 896 | # you run on the same table you had during the creation of the migration. 897 | # In future if you override the `Product` class 898 | # and change the `table_name`, it won't break 899 | # the migration or cause serious data corruption. 900 | class MigrationProduct < ActiveRecord::Base 901 | self.table_name = :products 902 | end 903 | 904 | class ModifyDefaultStatusForProducts < ActiveRecord::Migration 905 | def change 906 | old_status = 'pending_manual_approval' 907 | new_status = 'pending_approval' 908 | 909 | reversible do |dir| 910 | dir.up do 911 | MigrationProduct.where(status: old_status).update_all(status: new_status) 912 | change_column :products, :status, :string, default: new_status 913 | end 914 | 915 | dir.down do 916 | MigrationProduct.where(status: new_status).update_all(status: old_status) 917 | change_column :products, :status, :string, default: old_status 918 | end 919 | end 920 | end 921 | end 922 | ``` 923 | 924 | * 925 | Name your foreign keys explicitly instead of relying on Rails auto-generated 926 | FK names. (http://guides.rubyonrails.org/active_record_migrations.html#foreign-keys) 927 | [[link](#meaningful-foreign-key-naming)] 928 | 929 | ```Ruby 930 | # bad 931 | class AddFkArticlesToAuthors < ActiveRecord::Migration 932 | def change 933 | add_foreign_key :articles, :authors 934 | end 935 | end 936 | 937 | # good 938 | class AddFkArticlesToAuthors < ActiveRecord::Migration 939 | def change 940 | add_foreign_key :articles, :authors, name: :articles_author_id_fk 941 | end 942 | end 943 | ``` 944 | 945 | * 946 | Don't use non-reversible migration commands in the `change` method. 947 | Reversible migration commands are listed below. 948 | [ActiveRecord::Migration::CommandRecorder](http://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html) 949 | [[link](#reversible-migration)] 950 | 951 | ```ruby 952 | # bad 953 | class DropUsers < ActiveRecord::Migration 954 | def change 955 | drop_table :users 956 | end 957 | end 958 | 959 | # good 960 | class DropUsers < ActiveRecord::Migration 961 | def up 962 | drop_table :users 963 | end 964 | 965 | def down 966 | create_table :users do |t| 967 | t.string :name 968 | end 969 | end 970 | end 971 | 972 | # good 973 | # In this case, block will be used by create_table in rollback 974 | # http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table 975 | class DropUsers < ActiveRecord::Migration 976 | def change 977 | drop_table :users do |t| 978 | t.string :name 979 | end 980 | end 981 | end 982 | ``` 983 | 984 | * 985 | Avoid `FLOAT` to store rational numbers. `FLOAT` does not store precise values. Either convert the rational to a base unit (eg: store money in cents as an integer), or use the `DECIMAL` type, which does store precise values. 986 | [[link](#avoid-float)] 987 | 988 | ## Views 989 | 990 | * 991 | Never call the model layer directly from a view. 992 | [[link](#no-direct-model-view)] 993 | 994 | * 995 | Never make complex formatting in the views, export the formatting to a method 996 | in the decorator. 997 | [[link](#no-complex-view-formatting)] 998 | 999 | * 1000 | Mitigate code duplication by using partial templates and layouts. 1001 | [[link](#partials)] 1002 | 1003 | ## Internationalization 1004 | 1005 | * 1006 | No strings or other locale specific settings should be used in the views, 1007 | models and controllers. These texts should be moved to the locale files in the 1008 | `config/locales` directory. 1009 | [[link](#locale-texts)] 1010 | 1011 | * 1012 | When the labels of an ActiveRecord model need to be translated, use the 1013 | `activerecord` scope: 1014 | [[link](#translated-labels)] 1015 | 1016 | ``` 1017 | en: 1018 | activerecord: 1019 | models: 1020 | user: Member 1021 | attributes: 1022 | user: 1023 | name: 'Full name' 1024 | ``` 1025 | 1026 | Then `User.model_name.human` will return "Member" and 1027 | `User.human_attribute_name("name")` will return "Full name". These 1028 | translations of the attributes will be used as labels in the views. 1029 | 1030 | 1031 | * 1032 | Separate the texts used in the views from translations of ActiveRecord 1033 | attributes. Place the locale files for the models in a folder `locales/models` and the 1034 | texts used in the views in folder `locales/views`. 1035 | [[link](#organize-locale-files)] 1036 | 1037 | * When organization of the locale files is done with additional directories, 1038 | these directories must be described in the `application.rb` file in order 1039 | to be loaded. 1040 | 1041 | ```Ruby 1042 | # config/application.rb 1043 | config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] 1044 | ``` 1045 | 1046 | * 1047 | Place the shared localization options, such as date or currency formats, in 1048 | files under the root of the `locales` directory. 1049 | [[link](#shared-localization)] 1050 | 1051 | * 1052 | Use the short form of the I18n methods: `I18n.t` instead of `I18n.translate` 1053 | and `I18n.l` instead of `I18n.localize`. 1054 | [[link](#short-i18n)] 1055 | 1056 | * 1057 | Use "lazy" lookup for the texts used in views. Let's say we have the following 1058 | structure: 1059 | [[link](#lazy-lookup)] 1060 | 1061 | ``` 1062 | en: 1063 | users: 1064 | show: 1065 | title: 'User details page' 1066 | ``` 1067 | 1068 | The value for `users.show.title` can be looked up in the template 1069 | `app/views/users/show.html.haml` like this: 1070 | 1071 | ```Ruby 1072 | = t '.title' 1073 | ``` 1074 | 1075 | * 1076 | Use the dot-separated keys in the controllers and models instead of specifying 1077 | the `:scope` option. The dot-separated call is easier to read and trace the 1078 | hierarchy. 1079 | [[link](#dot-separated-keys)] 1080 | 1081 | ```Ruby 1082 | # bad 1083 | I18n.t :record_invalid, scope: [:activerecord, :errors, :messages] 1084 | 1085 | # good 1086 | I18n.t 'activerecord.errors.messages.record_invalid' 1087 | ``` 1088 | 1089 | * 1090 | More detailed information about the Rails I18n can be found in the [Rails 1091 | Guides](http://guides.rubyonrails.org/i18n.html) 1092 | [[link](#i18n-guides)] 1093 | 1094 | ## Assets 1095 | 1096 | Use the [assets pipeline](http://guides.rubyonrails.org/asset_pipeline.html) to leverage organization within 1097 | your application. 1098 | 1099 | * 1100 | Reserve `app/assets` for custom stylesheets, javascripts, or images. 1101 | [[link](#reserve-app-assets)] 1102 | 1103 | * 1104 | Use `lib/assets` for your own libraries that don’t really fit into the 1105 | scope of the application. 1106 | [[link](#lib-assets)] 1107 | 1108 | * 1109 | Third party code such as [jQuery](http://jquery.com/) or 1110 | [bootstrap](http://twitter.github.com/bootstrap/) should be placed in 1111 | `vendor/assets`. 1112 | [[link](#vendor-assets)] 1113 | 1114 | * 1115 | When possible, use gemified versions of assets (e.g. 1116 | [jquery-rails](https://github.com/rails/jquery-rails), 1117 | [jquery-ui-rails](https://github.com/joliss/jquery-ui-rails), 1118 | [bootstrap-sass](https://github.com/thomas-mcdonald/bootstrap-sass), 1119 | [zurb-foundation](https://github.com/zurb/foundation)). 1120 | [[link](#gem-assets)] 1121 | 1122 | ## Mailers 1123 | 1124 | * 1125 | Name the mailers `SomethingMailer`. Without the Mailer suffix it isn't 1126 | immediately apparent what's a mailer and which views are related to the 1127 | mailer. 1128 | [[link](#mailer-name)] 1129 | 1130 | * 1131 | Provide both HTML and plain-text view templates. 1132 | [[link](#html-plain-email)] 1133 | 1134 | * 1135 | Enable errors raised on failed mail delivery in your development environment. 1136 | The errors are disabled by default. 1137 | [[link](#enable-delivery-errors)] 1138 | 1139 | ```Ruby 1140 | # config/environments/development.rb 1141 | 1142 | config.action_mailer.raise_delivery_errors = true 1143 | ``` 1144 | 1145 | * 1146 | Use a local SMTP server like 1147 | [Mailcatcher](https://github.com/sj26/mailcatcher) in the development 1148 | environment. Or [Letter Opener](https://github.com/ryanb/letter_opener) that previews the email in the browser. 1149 | [[link](#local-smtp)] 1150 | 1151 | ```Ruby 1152 | # config/environments/development.rb 1153 | 1154 | config.action_mailer.smtp_settings = { 1155 | address: 'localhost', 1156 | port: 1025, 1157 | # more settings 1158 | } 1159 | ``` 1160 | 1161 | * 1162 | Provide default settings for the host name. 1163 | [[link](#default-hostname)] 1164 | 1165 | ```Ruby 1166 | # config/environments/development.rb 1167 | config.action_mailer.default_url_options = { host: "#{local_ip}:3000" } 1168 | 1169 | # config/environments/production.rb 1170 | config.action_mailer.default_url_options = { host: 'your_site.com' } 1171 | 1172 | # in your mailer class 1173 | default_url_options[:host] = 'your_site.com' 1174 | ``` 1175 | 1176 | * 1177 | If you need to use a link to your site in an email, always use the `_url`, not 1178 | `_path` methods. The `_url` methods include the host name and the `_path` 1179 | methods don't. 1180 | [[link](#url-not-path-in-email)] 1181 | 1182 | ```Ruby 1183 | # bad 1184 | You can always find more info about this course 1185 | <%= link_to 'here', course_path(@course) %> 1186 | 1187 | # good 1188 | You can always find more info about this course 1189 | <%= link_to 'here', course_url(@course) %> 1190 | ``` 1191 | 1192 | * 1193 | Format the from and to addresses properly. Use the following format: 1194 | [[link](#email-addresses)] 1195 | 1196 | ```Ruby 1197 | # in your mailer class 1198 | default from: 'Your Name ' 1199 | ``` 1200 | 1201 | * 1202 | Make sure that the e-mail delivery method for your test environment is set to 1203 | `test`: 1204 | [[link](#delivery-method-test)] 1205 | 1206 | ```Ruby 1207 | # config/environments/test.rb 1208 | 1209 | config.action_mailer.delivery_method = :test 1210 | ``` 1211 | 1212 | * 1213 | The delivery method for development and production should be `smtp`: 1214 | [[link](#delivery-method-smtp)] 1215 | 1216 | ```Ruby 1217 | # config/environments/development.rb, config/environments/production.rb 1218 | 1219 | config.action_mailer.delivery_method = :smtp 1220 | ``` 1221 | 1222 | * 1223 | When sending html emails all styles should be inline, as some mail clients 1224 | have problems with external styles. This however makes them harder to maintain 1225 | and leads to code duplication. There are two similar gems that transform the 1226 | styles and put them in the corresponding html tags: 1227 | [premailer-rails](https://github.com/fphilipe/premailer-rails) and 1228 | [roadie](https://github.com/Mange/roadie). 1229 | [[link](#inline-email-styles)] 1230 | 1231 | * 1232 | Sending emails while generating page response should be avoided. It causes 1233 | delays in loading of the page and request can timeout if multiple email are 1234 | sent. To overcome this emails can be sent in background process with the help 1235 | of [sidekiq](https://github.com/mperham/sidekiq) gem. 1236 | [[link](#background-email)] 1237 | 1238 | 1239 | ## Active Support Core Extensions 1240 | 1241 | * 1242 | Prefer Ruby 2.3's safe navigation operator `&.` over `ActiveSupport#try!`. 1243 | [[link](#try-bang)] 1244 | 1245 | ```ruby 1246 | # bad 1247 | obj.try! :fly 1248 | 1249 | # good 1250 | obj&.fly 1251 | ``` 1252 | 1253 | * 1254 | Prefer Ruby's Standard Library methods over `ActiveSupport` aliases. 1255 | [[link](#active_support_aliases)] 1256 | 1257 | ```ruby 1258 | # bad 1259 | 'the day'.starts_with? 'th' 1260 | 'the day'.ends_with? 'ay' 1261 | 1262 | # good 1263 | 'the day'.start_with? 'th' 1264 | 'the day'.end_with? 'ay' 1265 | ``` 1266 | 1267 | * 1268 | Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. 1269 | [[link](#active_support_extensions)] 1270 | 1271 | ```ruby 1272 | # bad 1273 | (1..50).to_a.forty_two 1274 | 1.in? [1, 2] 1275 | 'day'.in? 'the day' 1276 | 1277 | # good 1278 | (1..50).to_a[41] 1279 | [1, 2].include? 1 1280 | 'the day'.include? 'day' 1281 | ``` 1282 | 1283 | * 1284 | Prefer Ruby's comparison operators over ActiveSupport's `Array#inquiry`, `Numeric#inquiry` and `String#inquiry`. 1285 | [[link](#inquiry)] 1286 | 1287 | ```ruby 1288 | # bad - String#inquiry 1289 | ruby = 'two'.inquiry 1290 | ruby.two? 1291 | 1292 | # good 1293 | ruby = 'two' 1294 | ruby == 'two' 1295 | 1296 | # bad - Array#inquiry 1297 | pets = %w(cat dog).inquiry 1298 | pets.gopher? 1299 | 1300 | # good 1301 | pets = %w(cat dog) 1302 | pets.include? 'cat' 1303 | 1304 | # bad - Numeric#inquiry 1305 | 0.positive? 1306 | 0.negative? 1307 | 1308 | # good 1309 | 0 > 0 1310 | 0 < 0 1311 | ``` 1312 | 1313 | ## Time 1314 | 1315 | * 1316 | Config your timezone accordingly in `application.rb`. 1317 | [[link](#tz-config)] 1318 | 1319 | ```Ruby 1320 | config.time_zone = 'Eastern European Time' 1321 | # optional - note it can be only :utc or :local (default is :utc) 1322 | config.active_record.default_timezone = :local 1323 | ``` 1324 | 1325 | * 1326 | Don't use `Time.parse`. 1327 | [[link](#time-parse)] 1328 | 1329 | ```Ruby 1330 | # bad 1331 | Time.parse('2015-03-02 19:05:37') # => Will assume time string given is in the system's time zone. 1332 | 1333 | # good 1334 | Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00 1335 | ``` 1336 | 1337 | * 1338 | Don't use `Time.now`. 1339 | [[link](#time-now)] 1340 | 1341 | ```Ruby 1342 | # bad 1343 | Time.now # => Returns system time and ignores your configured time zone. 1344 | 1345 | # good 1346 | Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00 1347 | Time.current # Same thing but shorter. 1348 | ``` 1349 | 1350 | ## Bundler 1351 | 1352 | * 1353 | Put gems used only for development or testing in the appropriate group in the 1354 | Gemfile. 1355 | [[link](#dev-test-gems)] 1356 | 1357 | * 1358 | Use only established gems in your projects. If you're contemplating on 1359 | including some little-known gem you should do a careful review of its source 1360 | code first. 1361 | [[link](#only-good-gems)] 1362 | 1363 | * 1364 | OS-specific gems will by default result in a constantly changing 1365 | `Gemfile.lock` for projects with multiple developers using different operating 1366 | systems. Add all OS X specific gems to a `darwin` group in the Gemfile, and 1367 | all Linux specific gems to a `linux` group: 1368 | [[link](#os-specific-gemfile-locks)] 1369 | 1370 | ```Ruby 1371 | # Gemfile 1372 | group :darwin do 1373 | gem 'rb-fsevent' 1374 | gem 'growl' 1375 | end 1376 | 1377 | group :linux do 1378 | gem 'rb-inotify' 1379 | end 1380 | ``` 1381 | 1382 | To require the appropriate gems in the right environment, add the 1383 | following to `config/application.rb`: 1384 | 1385 | ```Ruby 1386 | platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym 1387 | Bundler.require(platform) 1388 | ``` 1389 | 1390 | * 1391 | Do not remove the `Gemfile.lock` from version control. This is not some 1392 | randomly generated file - it makes sure that all of your team members get the 1393 | same gem versions when they do a `bundle install`. 1394 | [[link](#gemfile-lock)] 1395 | 1396 | ## Managing processes 1397 | 1398 | * 1399 | If your projects depends on various external processes use 1400 | [foreman](https://github.com/ddollar/foreman) to manage them. 1401 | [[link](#foreman)] 1402 | 1403 | ## Logging 1404 | 1405 | When logging lot of output you can use a block as param instead of a string, specially if you have interpolated values. This avoids the cost of instantiating the unnecessary String objects if the allowed output level doesn't include that level. 1406 | 1407 | https://guides.rubyonrails.org/debugging_rails_applications.html#impact-of-logs-on-performance 1408 | 1409 | ```Ruby 1410 | # bad 1411 | Rails.logger.debug("Person attributes hash: #{@person.attributes.inspect}") 1412 | 1413 | # good 1414 | Rails.logger.debug { "Person attributes hash: #{@person.attributes.inspect}" } 1415 | ``` 1416 | 1417 | # References 1418 | 1419 | [Original Ruby style guide](https://github.com/bbatsov/rails-style-guide) 1420 | --------------------------------------------------------------------------------