Programming Conventions and Patterns
This document outlines the coding standards, conventions, and patterns used throughout the Roadtrip Planner application.
Ruby Conventions
Code Style
We follow Rails Omakase RuboCop configuration with these key principles:
Naming Conventions
# Classes: PascalCase
class RouteDistanceCalculator
end
# Methods and variables: snake_case
def calculate_distance_in_km
total_distance = 0
end
# Constants: SCREAMING_SNAKE_CASE
DEFAULT_ROUTE_DURATION = 2.hours
# Private methods: prefixed with private keyword
private
def helper_method
end
Method Organization
class Route < ApplicationRecord
# 1. Concerns and includes
include Validatable
# 2. Constants
DEFAULT_DURATION = 2.0
# 3. Associations
belongs_to :road_trip
belongs_to :user
# 4. Validations
validates :starting_location, presence: true
# 5. Callbacks
before_save :calculate_route_metrics
# 6. Scopes
scope :for_user, ->(user) { where(user: user) }
# 7. Public methods
def duration_hours
duration || DEFAULT_DURATION
end
# 8. Private methods
private
def calculate_route_metrics
# Implementation
end
end
String and Symbol Usage
# Prefer symbols for keys and internal identifiers
user_params = { username: 'john', password: 'secret' }
# Use strings for user-facing text
flash[:notice] = "Route successfully created"
# Hash syntax: new style for symbol keys
config = {
host: 'localhost',
port: 3000
}
# Old style only when keys are mixed types
config = {
:symbol_key => 'value',
'string_key' => 'value'
}
ActiveRecord Patterns
Model Organization
class User < ApplicationRecord
# Use has_secure_password for authentication
has_secure_password
# Associations with dependencies
has_many :road_trips, dependent: :destroy
has_many :routes, dependent: :destroy
# Validations with custom messages
validates :username,
presence: true,
length: { minimum: 3 },
uniqueness: { case_sensitive: false }
validates :password,
presence: true,
length: { minimum: 8 },
format: {
with: /\A(?=.*[a-zA-Z])(?=.*\d).*\z/,
message: "must contain both letters and numbers"
}
# Callbacks for data normalization
before_save :downcase_username
private
def downcase_username
self.username = username.downcase if username.present?
end
end
Query Patterns
# Use scopes for reusable queries
class RoadTrip < ApplicationRecord
scope :for_user, ->(user) { where(user: user) }
scope :recent, -> { order(created_at: :desc) }
end
# Chain scopes for complex queries
recent_trips = RoadTrip.for_user(current_user).recent.limit(10)
# Use includes for N+1 prevention
trips_with_routes = RoadTrip.includes(:routes).for_user(user)
# Use joins for filtering without loading associations
popular_trips = RoadTrip.joins(:routes).group('road_trips.id').having('count(routes.id) > 5')
Validation Patterns
# Custom validations as private methods
class Route < ApplicationRecord
validate :datetime_not_overlapping_with_other_routes
validate :user_matches_road_trip_user
private
def datetime_not_overlapping_with_other_routes
return unless datetime && road_trip
# Clear business logic with descriptive variable names
my_duration = duration || 2.0
end_time = datetime + my_duration.hours
overlapping_routes = road_trip.routes
.where.not(id: id)
.where(
"? < datetime + (COALESCE(duration, 2.0) * INTERVAL '1 hour') AND ? > datetime",
datetime, end_time
)
if overlapping_routes.exists?
errors.add(:datetime, "overlaps with another route in this road trip")
end
end
end
Phlex Component Patterns
Component Structure
class ButtonComponent < ApplicationComponent
# 1. Initialize with named parameters
def initialize(text:, variant: :primary, size: :medium, **options)
@text = text
@variant = variant
@size = size
@options = options
end
# 2. Main template method
def view_template
button(**attributes) { @text }
end
private
# 3. Attribute building
def attributes
{
class: button_classes,
**@options
}
end
# 4. CSS class logic
def button_classes
[
base_classes,
variant_classes,
size_classes
].join(' ')
end
def base_classes
"inline-flex items-center justify-center rounded-md font-medium transition-colors focus:outline-none focus:ring-2"
end
def variant_classes
case @variant
when :primary
"bg-primary-600 text-white hover:bg-primary-700 focus:ring-primary-500"
when :secondary
"bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500"
when :danger
"bg-red-600 text-white hover:bg-red-700 focus:ring-red-500"
end
end
def size_classes
case @size
when :small
"px-3 py-1.5 text-sm"
when :medium
"px-4 py-2 text-base"
when :large
"px-6 py-3 text-lg"
end
end
end
Component Composition
class CardComponent < ApplicationComponent
# Slots pattern for flexible content areas
def initialize(title: nil, **options)
@title = title
@options = options
end
def view_template(&block)
div(class: card_classes) do
header_section if @title
content_section(&block)
footer_section if footer_content?
end
end
private
def header_section
div(class: "px-6 py-4 border-b border-gray-200") do
h3(class: "text-lg font-medium text-gray-900") { @title }
end
end
def content_section(&block)
div(class: "px-6 py-4", &block)
end
def footer_section
div(class: "px-6 py-3 bg-gray-50 border-t border-gray-200") do
yield :footer if block_given?
end
end
def card_classes
"bg-white shadow rounded-lg border border-gray-200"
end
end
# Usage in other components
class TripDetailsComponent < ApplicationComponent
def view_template
render CardComponent.new(title: "Trip Details") do
p { "Trip content here" }
end
end
end
Form Components
class FormFieldComponent < ApplicationComponent
def initialize(form:, field:, label:, type: :text, **options)
@form = form
@field = field
@label = label
@type = type
@options = options
end
def view_template
div(class: field_wrapper_classes) do
label_element
input_element
error_messages if has_errors?
end
end
private
def label_element
@form.label(@field, @label, class: label_classes)
end
def input_element
case @type
when :text, :email, :password
@form.text_field(@field, **input_attributes)
when :textarea
@form.text_area(@field, **input_attributes)
when :select
@form.select(@field, @options[:choices], {}, input_attributes)
end
end
def input_attributes
{
class: input_classes,
**@options.except(:choices)
}
end
def field_wrapper_classes
"mb-4"
end
def label_classes
"block text-sm font-medium text-gray-700 mb-1"
end
def input_classes
base = "block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500"
has_errors? ? "#{base} border-red-300" : base
end
def has_errors?
@form.object.errors[@field].any?
end
def error_messages
div(class: "mt-1 text-sm text-red-600") do
@form.object.errors[@field].each do |error|
p { error }
end
end
end
end
Service Object Patterns
Service Organization
# Place services in app/services/
# Name services with descriptive action: VerbNoun
class RouteDistanceCalculator
# Result object pattern for complex returns
Result = Data.define(:distance, :duration, :success?, :error)
def initialize(start_location, end_location)
@start_location = start_location
@end_location = end_location
end
# Single public interface
def calculate
return invalid_locations_result if locations_invalid?
distance = calculate_distance
duration = calculate_duration(distance)
Result.new(
distance: distance,
duration: duration,
success?: true,
error: nil
)
rescue StandardError => e
Result.new(
distance: nil,
duration: nil,
success?: false,
error: e.message
)
end
private
def locations_invalid?
@start_location.blank? || @end_location.blank?
end
def invalid_locations_result
Result.new(
distance: nil,
duration: nil,
success?: false,
error: "Start and end locations are required"
)
end
def calculate_distance
# Distance calculation logic
# Return distance in kilometers
end
def calculate_duration(distance_km)
# Duration calculation logic
# Return duration in hours
end
end
# Usage pattern
result = RouteDistanceCalculator.new(start, destination).calculate
if result.success?
route.update!(distance: result.distance, duration: result.duration)
else
handle_error(result.error)
end
Controller Patterns
RESTful Controllers
class RoadTripsController < ApplicationController
# Use before_action for common operations
before_action :authenticate_user!
before_action :set_road_trip, only: [:show, :edit, :update, :destroy]
before_action :authorize_road_trip!, only: [:show, :edit, :update, :destroy]
def index
@road_trips = current_user.road_trips.recent.includes(:routes)
render RoadTripsIndexPage.new(road_trips: @road_trips)
end
def show
render RoadTripDetailsPage.new(road_trip: @road_trip)
end
def new
@road_trip = current_user.road_trips.build
render RoadTripFormPage.new(road_trip: @road_trip)
end
def create
@road_trip = current_user.road_trips.build(road_trip_params)
if @road_trip.save
redirect_to @road_trip, notice: 'Road trip was successfully created.'
else
render RoadTripFormPage.new(road_trip: @road_trip), status: :unprocessable_entity
end
end
private
def set_road_trip
@road_trip = RoadTrip.find(params[:id])
end
def authorize_road_trip!
redirect_to root_path unless @road_trip.user == current_user
end
def road_trip_params
params.require(:road_trip).permit(:name, :description)
end
end
Error Handling
class ApplicationController < ActionController::Base
# Global error handling
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found
render ErrorPage.new(status: 404, message: "Page not found"), status: :not_found
end
def bad_request
render ErrorPage.new(status: 400, message: "Bad request"), status: :bad_request
end
def authenticate_user!
redirect_to login_path unless current_user
end
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
end
Testing Patterns
Model Testing
# spec/models/user_spec.rb
RSpec.describe User, type: :model do
describe 'validations' do
subject { build(:user) }
it { is_expected.to validate_presence_of(:username) }
it { is_expected.to validate_length_of(:username).is_at_least(3) }
it { is_expected.to validate_uniqueness_of(:username).case_insensitive }
end
describe 'associations' do
it { is_expected.to have_many(:road_trips).dependent(:destroy) }
it { is_expected.to have_many(:routes).dependent(:destroy) }
end
describe '#downcase_username' do
it 'converts username to lowercase before saving' do
user = create(:user, username: 'TestUser')
expect(user.username).to eq('testuser')
end
end
end
Component Testing
# spec/components/button_component_spec.rb
RSpec.describe ButtonComponent, type: :component do
describe '#view_template' do
it 'renders a primary button' do
component = ButtonComponent.new(text: 'Click me', variant: :primary)
expect(component.view_template).to have_selector(
'button.bg-primary-600.text-white',
text: 'Click me'
)
end
it 'applies custom attributes' do
component = ButtonComponent.new(
text: 'Submit',
variant: :primary,
data: { action: 'form#submit' }
)
expect(component.view_template).to have_selector(
'button[data-action="form#submit"]'
)
end
end
end
Request Testing
# spec/requests/road_trips_spec.rb
RSpec.describe '/road_trips', type: :request do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
before { sign_in(user) }
describe 'GET /road_trips' do
it 'displays user road trips' do
road_trip = create(:road_trip, user: user)
other_trip = create(:road_trip, user: other_user)
get road_trips_path
expect(response).to have_http_status(:ok)
expect(response.body).to include(road_trip.name)
expect(response.body).not_to include(other_trip.name)
end
end
describe 'POST /road_trips' do
context 'with valid parameters' do
let(:valid_params) do
{ road_trip: { name: 'Summer Vacation' } }
end
it 'creates a new road trip' do
expect {
post road_trips_path, params: valid_params
}.to change(RoadTrip, :count).by(1)
expect(response).to redirect_to(RoadTrip.last)
end
end
context 'with invalid parameters' do
let(:invalid_params) do
{ road_trip: { name: '' } }
end
it 'does not create a road trip' do
expect {
post road_trips_path, params: invalid_params
}.not_to change(RoadTrip, :count)
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end
CSS/Tailwind Patterns
Utility Organization
/* Use @layer for organized custom styles */
@layer base {
/* Base element styles */
h1, h2, h3, h4, h5, h6 {
@apply font-semibold text-gray-900;
}
}
@layer components {
/* Reusable component styles */
.btn {
@apply inline-flex items-center justify-center rounded-md font-medium transition-colors;
}
.btn-primary {
@apply btn bg-primary-600 text-white hover:bg-primary-700;
}
.form-input {
@apply block w-full rounded-md border-gray-300 shadow-sm focus:border-primary-500 focus:ring-primary-500;
}
}
@layer utilities {
/* Custom utility classes */
.text-balance {
text-wrap: balance;
}
}
Responsive Design Patterns
# Use consistent responsive patterns in components
def responsive_classes
[
# Mobile first approach
"text-sm", # Base size
"md:text-base", # Medium screens and up
"lg:text-lg", # Large screens and up
"px-4 py-2", # Base padding
"md:px-6 md:py-3", # Larger padding on medium+
].join(' ')
end
Documentation Patterns
Code Documentation
# Use clear, descriptive comments for complex logic
class Route < ApplicationRecord
# Validates that route datetime doesn't overlap with other routes
# in the same road trip. Two routes overlap if:
# route1.start < route2.end AND route1.end > route2.start
validate :datetime_not_overlapping_with_other_routes
# Calculates the total distance for this route using external APIs
# Falls back to stored distance if API is unavailable
# @return [Float] distance in kilometers
def distance_in_km
distance || calculate_and_save_route_metrics[:distance] || 0.0
end
end
README Patterns
# Clear section headers
## Installation
## Usage
## Contributing
## License
# Use code blocks with language specification
```ruby
user = User.create!(username: 'john', password: 'secret123')
Include examples and use cases
Creating a Road Trip
trip = current_user.road_trips.create!(name: 'Summer Adventure')
These conventions ensure consistency, maintainability, and readability across the entire codebase. All team members should follow these patterns when contributing to the project.