GeneralStandards
Coding Standards & Style Guide
This document outlines the coding standards, conventions, and best practices for the Kulcare API Backend codebase.
Ruby Style Guide
We follow the community Ruby Style Guide with some project-specific modifications.
Code Formatting
Indentation and Spacing
# Use 2 spaces for indentation (no tabs)
class User < ApplicationRecord
has_many :clinics
def full_name
"#{first_name} #{last_name}"
end
end
# Line length: 100 characters max
def long_method_name(parameter_one, parameter_two, parameter_three)
# Break long lines logically
some_very_long_method_call(
parameter_one,
parameter_two,
parameter_three
)
end
# Align parameters when breaking lines
User.where(
country_code: '91',
verified: true,
created_at: 1.week.ago..Time.current
)String Literals
# Use single quotes for simple strings
name = 'John Doe'
# Use double quotes when interpolation or escape sequences needed
message = "Hello #{name}!"
error_message = "Error: \n#{error.message}"
# Use %w for word arrays
VALID_STATUSES = %w[pending confirmed completed cancelled].freeze
# Use heredoc for multi-line strings
email_template = <<~HTML
<html>
<body>
<h1>Welcome to Kulcare!</h1>
</body>
</html>
HTMLNaming Conventions
Variables and Methods
# Use snake_case for variables and methods
user_count = 10
appointment_date = Date.today
def calculate_consultation_fee
base_fee + additional_charges
end
# Use descriptive names
# Bad
def calc(u, d)
u.appts.where(date: d).count
end
# Good
def count_appointments_for_date(user, date)
user.appointments.where(appointment_date: date).count
end
# Boolean methods end with ?
def verified?
verification_status == 'verified'
end
# Dangerous methods (destructive) end with !
def verify_user!
update!(verified: true)
send_welcome_email
endConstants and Classes
# Use SCREAMING_SNAKE_CASE for constants
MAX_APPOINTMENT_DURATION = 60.minutes
DEFAULT_COUNTRY_CODE = '91'
# Use PascalCase for classes and modules
class AppointmentScheduler
module TimeSlotCalculator
end
end
# Use descriptive class names
class CreateAppointment < BaseInteractor # Good
class CA < BI # BadMethod Definitions
# Prefer explicit return for clarity in complex methods
def calculate_total_fee
base_fee = consultation_fee
additional_fee = calculate_additional_charges
return 0 if patient.has_free_consultation?
base_fee + additional_fee
end
# Use keyword arguments for methods with multiple parameters
def create_appointment(patient:, doctor:, date:, time:, notes: nil)
Appointment.create!(
patient: patient,
doctor: doctor,
appointment_date: date,
appointment_time: time,
notes: notes
)
end
# Use default parameter values when appropriate
def send_notification(message, delivery_method: :sms, urgent: false)
# implementation
endException Handling
# Use specific exception classes
class InvalidAppointmentTime < StandardError; end
class PatientNotFound < StandardError; end
# Handle exceptions gracefully
def book_appointment(appointment_params)
validate_appointment_time(appointment_params[:time])
create_appointment(appointment_params)
rescue InvalidAppointmentTime => e
Rails.logger.warn "Invalid appointment time: #{e.message}"
{ success: false, error: e.message }
rescue StandardError => e
Rails.logger.error "Unexpected error: #{e.message}"
{ success: false, error: 'Something went wrong' }
end
# Use ensure for cleanup
def process_medical_document
temp_file = create_temp_file
process_file(temp_file)
ensure
temp_file&.close
temp_file&.unlink
endRails Conventions
Models
ActiveRecord Models
class Appointment < ApplicationRecord
# Constants first
VALID_STATUSES = %w[scheduled confirmed completed cancelled].freeze
# Associations
belongs_to :clinic
belongs_to :patient
belongs_to :doctor
# Validations
validates :appointment_date, presence: true
validates :appointment_time, presence: true
validates :status, inclusion: { in: VALID_STATUSES }
# Scopes
scope :today, -> { where(appointment_date: Date.current) }
scope :upcoming, -> { where('appointment_date >= ?', Date.current) }
scope :by_status, ->(status) { where(status: status) }
# Callbacks (in order of execution)
before_validation :set_default_status
before_create :generate_confirmation_code
after_create :send_confirmation
after_update :handle_status_change
# Class methods
def self.available_slots(doctor, date)
# implementation
end
# Instance methods
def can_be_cancelled?
scheduled? && appointment_date > Date.current
end
def display_time
appointment_time.strftime('%I:%M %p')
end
private
# Private methods last
def set_default_status
self.status ||= 'scheduled'
end
endModel Validations
class User < ApplicationRecord
# Use appropriate validation types
validates :phone_number,
presence: true,
uniqueness: { scope: :country_code },
format: { with: /\A\d{10,15}\z/ }
validates :email,
format: { with: URI::MailTo::EMAIL_REGEXP },
allow_blank: true
validates :password,
length: { minimum: 8 },
format: {
with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+\z/,
message: 'must include uppercase, lowercase, and digit'
},
if: :password_required?
# Custom validations
validate :phone_number_format
validate :future_birth_date, if: :date_of_birth?
private
def phone_number_format
return unless phone_number.present?
unless phone_number.match?(/\A\d{10,15}\z/)
errors.add(:phone_number, 'must be 10-15 digits')
end
end
def future_birth_date
return unless date_of_birth.present?
if date_of_birth > Date.current
errors.add(:date_of_birth, 'cannot be in the future')
end
end
endControllers
Parts-Based Controllers
# parts/clinic/controllers/appointments_controller.rb
class AppointmentsController < ApplicationController
before_action :authenticate_user!
before_action :set_appointment, only: [:show, :update, :destroy]
before_action :authorize_appointment, only: [:show, :update, :destroy]
# Use consistent action structure
def index
result = FetchAppointments.call(
clinic: current_clinic,
params: filter_params
)
if result.success?
render json: result.appointments,
each_serializer: AppointmentSerializer,
meta: pagination_meta(result.appointments)
else
render_error(result.error)
end
end
def show
render json: @appointment, serializer: AppointmentDetailSerializer
end
def create
result = CreateAppointment.call(
appointment_params.merge(
clinic: current_clinic,
current_user: current_user
)
)
if result.success?
render json: result.appointment,
serializer: AppointmentSerializer,
status: :created
else
render_error(result.error, :unprocessable_entity)
end
end
def update
result = UpdateAppointment.call(
appointment: @appointment,
params: appointment_params,
current_user: current_user
)
if result.success?
render json: result.appointment, serializer: AppointmentSerializer
else
render_error(result.error, :unprocessable_entity)
end
end
def destroy
result = CancelAppointment.call(
appointment: @appointment,
current_user: current_user
)
if result.success?
head :no_content
else
render_error(result.error)
end
end
private
def set_appointment
@appointment = current_clinic.appointments.find(params[:id])
rescue ActiveRecord::RecordNotFound
render_error('Appointment not found', :not_found)
end
def authorize_appointment
authorize @appointment
end
def appointment_params
params.require(:appointment).permit(
:patient_id, :doctor_id, :appointment_date, :appointment_time,
:duration, :notes, :status
)
end
def filter_params
params.permit(:date, :status, :doctor_id, :page, :per_page)
end
endInteractors
Interactor Structure
# parts/clinic/interactors/create_appointment.rb
class CreateAppointment < BaseInteractor
# Define expected context early
delegate :patient_id, :doctor_id, :appointment_date, :clinic, :current_user,
to: :context
def call
validate_inputs
check_availability
create_appointment
send_notifications
log_activity
rescue StandardError => e
Rails.logger.error "CreateAppointment failed: #{e.message}"
context.fail!(error: e.message)
end
private
# Keep methods focused and single-purpose
def validate_inputs
validate_required_fields
validate_appointment_date
validate_appointment_time
end
def validate_required_fields
context.fail!(error: 'Patient is required') unless patient_id.present?
context.fail!(error: 'Doctor is required') unless doctor_id.present?
context.fail!(error: 'Date is required') unless appointment_date.present?
end
def validate_appointment_date
if appointment_date < Date.current
context.fail!(error: 'Appointment date cannot be in the past')
end
end
def check_availability
return unless doctor_busy?
context.fail!(error: 'Doctor is not available at this time')
end
def doctor_busy?
clinic.appointments
.where(doctor_id: doctor_id)
.where(appointment_date: appointment_date)
.where(appointment_time: context.appointment_time)
.exists?
end
def create_appointment
context.appointment = Appointment.create!(
clinic: clinic,
patient_id: patient_id,
doctor_id: doctor_id,
appointment_date: appointment_date,
appointment_time: context.appointment_time,
status: 'scheduled',
created_by: current_user
)
end
def send_notifications
AppointmentConfirmationJob.perform_later(context.appointment.id)
end
def log_activity
Rails.logger.info "Appointment created: #{context.appointment.id}"
end
endServices
Service Object Structure
# parts/clinic/services/appointment_scheduler.rb
class AppointmentScheduler
include ActiveModel::Validations
attr_reader :clinic, :doctor, :date
validates :clinic, :doctor, :date, presence: true
validate :date_not_in_past
def initialize(clinic:, doctor:, date:)
@clinic = clinic
@doctor = doctor
@date = date
end
def available_slots
return [] unless valid?
@available_slots ||= calculate_available_slots
end
def next_available_slot
available_slots.first
end
def book_slot(time_slot, patient)
validate_slot_availability(time_slot)
Appointment.create!(
clinic: clinic,
doctor: doctor,
patient: patient,
appointment_date: date,
appointment_time: time_slot
)
end
private
def calculate_available_slots
all_possible_slots - booked_slots
end
def all_possible_slots
schedule = doctor.schedule_for_date(date)
return [] unless schedule
slots = []
current_time = schedule.start_time
while current_time < schedule.end_time
slots << current_time
current_time += schedule.slot_duration.minutes
end
slots
end
def booked_slots
clinic.appointments
.where(doctor: doctor, appointment_date: date)
.pluck(:appointment_time)
end
def validate_slot_availability(time_slot)
unless available_slots.include?(time_slot)
raise ArgumentError, 'Time slot not available'
end
end
def date_not_in_past
return unless date.present?
if date < Date.current
errors.add(:date, 'cannot be in the past')
end
end
endDatabase Conventions
Migrations
# Use descriptive migration names
# Good: 20240115120000_add_verification_status_to_users.rb
# Bad: 20240115120000_update_users.rb
class AddVerificationStatusToUsers < ActiveRecord::Migration[7.1]
def change
add_column :users, :verification_status, :string, default: 'pending'
add_column :users, :verified_at, :timestamp
add_index :users, :verification_status
end
end
# Use reversible migrations when needed
class UpdateExistingUserStatuses < ActiveRecord::Migration[7.1]
def up
User.where(verified: true).update_all(
verification_status: 'verified',
verified_at: Time.current
)
end
def down
User.update_all(
verification_status: 'pending',
verified_at: nil
)
end
end
# Use safe migrations in production
class AddIndexConcurrently < ActiveRecord::Migration[7.1]
disable_ddl_transaction!
def change
add_index :appointments, :clinic_id, algorithm: :concurrently
end
endDatabase Queries
# Use includes to prevent N+1 queries
appointments = clinic.appointments
.includes(:patient, :doctor)
.where(appointment_date: Date.current)
# Use specific selects when you don't need all columns
patient_names = clinic.patients
.select(:id, :first_name, :last_name)
.active
# Use scopes for common queries
class Appointment < ApplicationRecord
scope :today, -> { where(appointment_date: Date.current) }
scope :upcoming, -> { where('appointment_date >= ?', Date.current) }
scope :for_doctor, ->(doctor) { where(doctor: doctor) }
end
# Chain scopes for readable queries
appointments = Appointment.today.for_doctor(doctor).upcomingPerformance Best Practices
Query Optimization
# Use counter caches for expensive counts
class Clinic < ApplicationRecord
has_many :appointments, counter_cache: true
has_many :patients, counter_cache: true
end
# Use exists? instead of present? for existence checks
# Bad
if user.appointments.any?
# do something
end
# Good
if user.appointments.exists?
# do something
end
# Use pluck for single column selections
patient_ids = clinic.patients.active.pluck(:id)
# Use find_each for batch processing
Patient.find_each(batch_size: 1000) do |patient|
# Process each patient
endMemory Management
# Use lazy enumeration for large datasets
def export_appointments
appointments = Appointment.includes(:patient, :doctor)
CSV.generate do |csv|
appointments.find_each do |appointment|
csv << [appointment.id, appointment.patient.name, appointment.doctor.name]
end
end
end
# Use delegate to avoid unnecessary object creation
class AppointmentPresenter
delegate :id, :appointment_date, :status, to: :appointment
def initialize(appointment)
@appointment = appointment
end
private
attr_reader :appointment
endSecurity Best Practices
Parameter Sanitization
# Always use strong parameters
def appointment_params
params.require(:appointment).permit(
:patient_id, :doctor_id, :appointment_date, :appointment_time,
:notes, :duration
)
end
# Validate and sanitize inputs
class AppointmentForm
include ActiveModel::Model
attr_accessor :patient_id, :doctor_id, :appointment_date
validates :patient_id, presence: true, numericality: { greater_than: 0 }
validates :appointment_date, presence: true
validate :appointment_date_format
private
def appointment_date_format
return unless appointment_date.present?
Date.parse(appointment_date.to_s)
rescue ArgumentError
errors.add(:appointment_date, 'is invalid')
end
endSQL Injection Prevention
# Use parameterized queries
# Bad
User.where("name = '#{params[:name]}'")
# Good
User.where(name: params[:name])
User.where('created_at > ?', 1.week.ago)
# Use Arel for complex queries
users = User.arel_table
query = users[:name].matches("%#{search_term}%")
.and(users[:verified].eq(true))
User.where(query)Testing Standards
Test Organization
# spec/parts/clinic/models/appointment_spec.rb
RSpec.describe Appointment, type: :model do
# Use descriptive describe blocks
describe 'associations' do
it { should belong_to(:clinic) }
it { should belong_to(:patient) }
it { should belong_to(:doctor) }
end
describe 'validations' do
subject { build(:appointment) }
it { should validate_presence_of(:appointment_date) }
it { should validate_inclusion_of(:status).in_array(described_class::VALID_STATUSES) }
end
describe '#can_be_cancelled?' do
context 'when appointment is scheduled' do
let(:appointment) { build(:appointment, status: 'scheduled') }
context 'when appointment is in the future' do
before { appointment.appointment_date = Date.tomorrow }
it 'returns true' do
expect(appointment.can_be_cancelled?).to be true
end
end
context 'when appointment is today' do
before { appointment.appointment_date = Date.current }
it 'returns false' do
expect(appointment.can_be_cancelled?).to be false
end
end
end
end
endDocumentation Standards
Code Comments
# Use comments for complex business logic
class ConsultationFeeCalculator
# Calculates consultation fee based on doctor's base rate,
# patient's insurance coverage, and any applicable discounts.
#
# Returns the final fee amount after all adjustments.
def calculate_fee(doctor, patient, appointment_type)
base_fee = doctor.consultation_fee
# Apply insurance discount if patient has active coverage
if patient.has_active_insurance?
base_fee *= (1 - patient.insurance_coverage_percentage / 100.0)
end
# Apply first-time patient discount
if first_time_patient?(patient)
base_fee *= 0.9 # 10% discount
end
base_fee.round(2)
end
private
def first_time_patient?(patient)
patient.appointments.completed.count.zero?
end
end
# Document API endpoints
class AppointmentsController < ApplicationController
# GET /appointments
#
# Fetches appointments for the current clinic with optional filtering.
#
# Parameters:
# - date: Filter by specific date (YYYY-MM-DD)
# - status: Filter by appointment status
# - doctor_id: Filter by specific doctor
# - page: Page number for pagination
#
# Returns: Paginated list of appointments with patient and doctor details
def index
# implementation
end
endREADME Documentation
# Feature Name
Brief description of what this feature does.
## Installation
Steps to set up the feature.
## Usage
```ruby
# Code examples
result = CreateAppointment.call(
patient_id: 1,
doctor_id: 2,
appointment_date: Date.tomorrow
)API Endpoints
GET /appointments- List appointmentsPOST /appointments- Create appointment
Configuration
Required environment variables or settings.
## Error Handling
### Custom Exceptions
```ruby
# Define domain-specific exceptions
module Appointments
class Error < StandardError; end
class TimeSlotUnavailable < Error; end
class InvalidDate < Error; end
class DoctorNotFound < Error; end
end
# Use specific exception handling
def create_appointment(params)
validate_doctor_exists(params[:doctor_id])
validate_time_slot(params[:time])
# ... create appointment
rescue Appointments::DoctorNotFound => e
{ success: false, error: 'Doctor not found' }
rescue Appointments::TimeSlotUnavailable => e
{ success: false, error: 'Time slot is not available' }
rescue StandardError => e
Rails.logger.error "Unexpected error: #{e.message}"
{ success: false, error: 'Something went wrong' }
endCode Review Checklist
Before Submitting
- Code follows style guide
- All tests pass
- Test coverage is adequate (>80%)
- No security vulnerabilities
- Performance considerations addressed
- Documentation updated
- Error handling implemented
- Logging added where appropriate
Reviewer Checklist
- Code is readable and maintainable
- Architecture patterns followed
- No code duplication
- Edge cases handled
- Database queries optimized
- API contracts maintained
- Breaking changes documented
Tools and Linters
RuboCop Configuration
# .rubocop.yml
inherit_from: .rubocop_todo.yml
AllCops:
TargetRubyVersion: 3.2
NewCops: enable
Exclude:
- 'db/schema.rb'
- 'db/migrate/*'
- 'vendor/**/*'
- 'node_modules/**/*'
Layout/LineLength:
Max: 100
AllowedPatterns: ['\A\s*#.*\z']
Style/Documentation:
Enabled: false
Style/StringLiterals:
EnforcedStyle: single_quotes
Style/TrailingCommaInArrayLiteral:
EnforcedStyleForMultiline: comma
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: commaRunning Linters
# Check code style
bundle exec rubocop
# Auto-fix issues
bundle exec rubocop -a
# Check specific files
bundle exec rubocop parts/clinic/controllers/Continuous Integration
GitHub Actions
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.0
bundler-cache: true
- name: Run RuboCop
run: bundle exec rubocop
- name: Run tests
run: bundle exec rspecThis coding standards guide ensures consistency across the Kulcare codebase and helps maintain high code quality as the team grows.