Ruby Standards
Ruby Standards
Section titled “Ruby Standards”1. Package Management
Section titled “1. Package Management”- Gems:
bundler.Gemfile.lockcommitted. Pin exact versions for production dependencies. - Ruby Version: Ruby 3.2+. Use
misefor version management. Specify version in.ruby-version. - Config:
Gemfilefor all projects. Group gems by environment (:development,:test,:production). - Age Gate: Do not adopt any gem version published less than 3 days ago. Verify publish date via RubyGems API before upgrading. See
sec-01§5 for exceptions.
2. Code Style
Section titled “2. Code Style”- Linter/Formatter:
rubocop. Run viamake lintorbundle exec rubocop. - Auto-correct:
rubocop -afor safe auto-corrections only. Run viamake fmt. Userubocop -Afor unsafe auto-corrections when you’ve reviewed the changes. - Config:
.rubocop.ymlat project root. EnableNewCops.
.rubocop.yml (base)
Section titled “.rubocop.yml (base)”AllCops: NewCops: enable SuggestExtensions: false Exclude: - 'bin/**/*' - 'script/**/*'
Style/Documentation: Enabled: false
Style/HashSyntax: EnforcedShorthandSyntax: never
Layout/MultilineMethodCallIndentation: EnforcedStyle: indented
Layout/FirstHashElementIndentation: EnforcedStyle: consistent3. Naming Conventions
Section titled “3. Naming Conventions”- Files:
snake_case.rb - Classes/Modules:
PascalCase(CamelCase) - Methods/Variables:
snake_case - Constants:
UPPER_SNAKE_CASE - Predicates:
?suffix —active?,valid_email? - Dangerous methods:
!suffix —save!,destroy! - Private: No prefix convention. Use
privatekeyword.
4. Type Safety — Sorbet
Section titled “4. Type Safety — Sorbet”Ruby code MUST use Sorbet for static typing. This is not optional — every file must include a typed sigil and every method must have a signature.
Requirements
Section titled “Requirements”- Typed sigil: All files must have
# typed: strictat the top. - Method signatures: All methods must have
sigannotations — no exceptions. - Variables: Use
T.letfor variables where the type is ambiguous. - Nullable: Use
T.nilable(Type)for nullable values. - No
T.untypedwithout explicit justification in a comment. - RBI generation: Use
tapiocafor generating RBI files. Store insorbet/rbi/.
Exceptions
Section titled “Exceptions”- Spec files (
spec/):# typed: falseis acceptable. Test files use dynamic matchers and DSLs that are incompatible with strict mode. - Migrations (
db/migrate/):# typed: falseis acceptable. Generated code. - Config files (
config/):# typed: ignoreis acceptable for Rails-generated config files.
sorbet/config
Section titled “sorbet/config”--dir.--ignore=vendor/Example
Section titled “Example”# typed: strict# frozen_string_literal: true
class Email extend T::Sig
sig { returns(String) } attr_reader :value
sig { params(value: String).void } def initialize(value) raise InvalidEmailError, value unless value.include?('@') && value.include?('.')
@value = T.let(value, String) end
# T.untyped: Ruby's == can receive any object type by convention sig { params(other: T.untyped).returns(T::Boolean) } def ==(other) other.is_a?(Email) && other.value == value end
sig { returns(String) } def to_s value endend5. Project Structure
Section titled “5. Project Structure”app/├── domain/│ ├── entities/│ │ └── email.rb│ └── value_objects/│ └── money.rb├── application/│ ├── use_cases/│ │ └── create_user.rb│ └── interfaces/│ └── user_repository.rb├── infrastructure/│ ├── repositories/│ │ └── pg_user_repository.rb│ └── adapters/│ └── stripe_adapter.rblib/├── core_ext/│ └── string.rb└── utils/ └── validator.rbspec/├── domain/│ └── entities/│ └── email_spec.rb├── application/│ └── use_cases/│ └── create_user_spec.rb└── spec_helper.rb6. Testing
Section titled “6. Testing”- Methodology: Test-Driven Development (TDD) is mandatory. Write failing tests before implementation code.
- Framework:
rspec. Useletandsubjectfor test setup. Preferdescribe/context/itblocks. - Factories:
factory_botfor test data. No fixtures — use factories exclusively. - Matchers:
shoulda-matchersfor common Rails matchers. - Coverage:
simplecov. 95% is the absolute minimum for any module. Target 100% for domain, 95%+ for application and infrastructure. - Regression: Every bug fix must include a regression test.
Test Structure
Section titled “Test Structure”# typed: false# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Email do describe '#initialize' do context 'with a valid email' do subject { described_class.new('user@example.com') }
it 'creates an email' do expect(subject.value).to eq('user@example.com') end end
context 'with an invalid email' do it 'raises InvalidEmailError' do expect { described_class.new('invalid') }.to raise_error(InvalidEmailError) end end endend7. Error Handling
Section titled “7. Error Handling”- Custom Exceptions: Define in domain layer. Inherit from a domain base exception.
- No bare
rescue: Always specify the exception class. Never userescue => ewithout a type. - Exception chaining: Wrap lower-level errors with domain-specific exceptions.
# typed: strict# frozen_string_literal: true
class DomainError < StandardError extend T::Sig
sig { returns(T.nilable(Exception)) } attr_reader :cause
sig { params(message: String, cause: T.nilable(Exception)).void } def initialize(message, cause: nil) super(message) @cause = T.let(cause, T.nilable(Exception)) endend
class InvalidEmailError < DomainError extend T::Sig
sig { params(email: String).void } def initialize(email) super("Invalid email address: #{email}") endend8. Documentation
Section titled “8. Documentation”- Format: YARD. Required for all public classes and methods.
- Type Info: Sorbet
sigblocks are the source of truth for types. YARD@paramand@returntags supplement with descriptions. - Examples: Include usage examples for complex methods.
# typed: strict# frozen_string_literal: true
class UserService extend T::Sig
# Creates a new user with a validated email address. # # @param email [String] valid email address (must contain @ and .) # @param name [String] user's full name (non-empty) # @return [User] newly created user entity # @raise [InvalidEmailError] if the email format is invalid # @raise [DuplicateUserError] if a user with this email already exists # # @example # service = UserService.new(repo) # user = service.create_user("test@example.com", "John Doe") # user.email #=> "test@example.com" # sig { params(email: String, name: String).returns(User) } def create_user(email, name) # Implementation endend9. Dependencies
Section titled “9. Dependencies”Common Gems
Section titled “Common Gems”- HTTP:
faraday(client with middleware),httparty(simple HTTP) - Database:
pg(PostgreSQL driver),sequel(lightweight ORM),rom-rb(data mapper) - Serialization:
oj(fast JSON) - Background Jobs:
sidekiqfor async job processing - Logging:
semantic_loggerfor structured logging - Type Safety:
sorbet(runtime),tapioca(RBI generation)
10. Async
Section titled “10. Async”- Thread Safety:
concurrent-rubyfor thread-safe data structures (Concurrent::Hash,Concurrent::Array,Concurrent::Future). - Fiber Scheduler: Ruby 3.0+ Fiber Scheduler for non-blocking I/O. Use
Asyncgem for structured concurrency. - Background Jobs:
sidekiqfor background job processing. Keep jobs idempotent and small.
# typed: strict# frozen_string_literal: true
require 'concurrent'
class FetchUsersService extend T::Sig
sig { params(ids: T::Array[String]).returns(T::Array[User]) } def fetch_all(ids) futures = ids.map do |id| Concurrent::Future.execute { fetch_user(id) } end
futures.map(&:value!) end
private
sig { params(id: String).returns(User) } def fetch_user(id) # Implementation endend11. Security
Section titled “11. Security”Full security standards:
standards/security/sec-01_security_standards.md
- SAST: Use
rubocop-securityrules for all Ruby projects. For Rails apps, also runbrakeman --no-pagerin CI. - Dependency scanning: Run
bundle-audit check --updatein CI. - Secrets scanning: Use
detect-secretsas a pre-commit hook. - Banned functions: See sec-01_security_standards.md for the complete banned-functions list with language-specific examples.
- Secure random: Use
SecureRandom.hex()orSecureRandom.uuid(), notrand(), for security contexts.