# Security Best Practices Standards for Crystal
This document outlines security best practices for Crystal development, serving as a guide for developers and AI coding assistants. It focuses on protecting against common vulnerabilities and implementing secure coding patterns specific to Crystal, embracing modern approaches based on the latest version.
## 1. Input Validation and Sanitization
### Standard
Validate and sanitize all external input to prevent injection attacks (SQL injection, XSS, command injection).
### Why
Untrusted data from users or external systems can corrupt data, execute malicious code, or compromise the system.
### Guidelines
* **Do This:**
* Always validate input data types, lengths, ranges, and formats against expected values.
* Use prepared statements and parameterized queries for database interactions.
* Encode output to prevent XSS vulnerabilities.
* Use regular expressions or dedicated libraries for input validation.
* **Don't Do This:**
* Trust user input without validation.
* Concatenate user input directly into SQL queries.
* Display user-provided data directly without encoding.
* Rely solely on client-side validation.
### Code Examples
**SQL Injection Prevention (Prepared Statements)**
"""crystal
require "db"
DB.open("sqlite3:mydb.db") do |db|
# Do This: Use prepared statements
statement = db.prepare("SELECT * FROM users WHERE username = ? AND password = ?")
username = "user1"
password = "password123"
result_set = statement.execute(username, password)
result_set.each do |row|
puts "User found: #{row}"
end
# Don't Do This: Avoid string interpolation
# username = "user1; DROP TABLE users;" # Malicious input
# query = "SELECT * FROM users WHERE username = '#{username}'" # Vulnerable!
# db.query(query).each do |row|
# puts "User found: #{row}"
# end
end
"""
**XSS Prevention (Output Encoding)**
"""crystal
require "kemal"
get "/" do |env|
user_input = env.params["query"] || "Default Value" # Assume "query" parameter contains user input.
# Do This: Escape HTML entities to prevent XSS
escaped_input = CGI.escape_html(user_input)
"You searched for: #{escaped_input}"
end
"""
**Command Injection Prevention**
"""crystal
# Do This: Use shell-safe methods or avoid shell commands whenever possible.
require "process"
def execute_safe_command(file_path : String)
# Use Process::run to avoid shell interpretation of arguments
result = Process.run("ls", [file_path])
if result.success?
puts result.output
else
puts result.error
end
end
# Example usage with a safe file path
execute_safe_command("/safe/path/to/file.txt")
# Don't Do This: Directly pass unsanitized user input to shell commands
# def execute_command(user_input : String)
# "ls #{user_input}" # Highly vulnerable to command injection
# end
# Example of VERY UNSAFE usage (demonstration purposes only!)
# execute_command("$(rm -rf /)") # Example of malicious input
"""
### Anti-Patterns
* String interpolation directly in SQL queries.
* Instead, always use prepared statements.
* Trusting client-side validation without server-side verification.
* Client-side validation is easily bypassed.
* Displaying raw user input without encoding.
* This can lead to XSS vulnerabilities.
## 2. Authentication and Authorization
### Standard
Implement robust authentication and authorization mechanisms to identify and control user access.
### Why
Weak authentication and authorization can lead to unauthorized access, data breaches, and compromised functionality.
### Guidelines
* **Do This:**
* Use strong password hashing algorithms (bcrypt, scrypt, argon2).
* Implement multi-factor authentication (MFA).
* Enforce principle of least privilege.
* Use established authentication/authorization libraries (e.g., JWT, OAuth).
* **Don't Do This:**
* Store passwords in plain text.
* Use weak hashing algorithms (MD5, SHA1).
* Grant excessive permissions to users.
* Implement custom authentication/authorization schemes without thorough security review.
### Code Examples
**Password Hashing with bcrypt**
"""crystal
require "bcrypt"
def hash_password(password : String) : String
BCrypt::Password.create(password)
end
def verify_password(password : String, hashed_password : String) : Bool
BCrypt::Password.new(hashed_password) == password
end
# Example usage
password = "mysecretpassword"
hashed_password = hash_password(password)
puts "Hashed password: #{hashed_password}"
# Store the 'hashed_password' in database
# Verification
if verify_password("mysecretpassword", hashed_password)
puts "Authentication successful!"
else
puts "Authentication failed!"
end
"""
**JWT Authentication (using "jwt" shard)**
"""crystal
require "jwt"
require "time"
SECRET_KEY = "your_secret_key" # Replace with a strong, randomly generated key
def generate_token(user_id : Int32) : String
payload = {
"user_id" => user_id,
"exp" => (Time.utc + 3600).to_unix # Token expiration time (1 hour)
}
JWT.encode(payload, SECRET_KEY, "HS256")
end
def verify_token(token : String) : Hash(String, JSON::Any)?
begin
decoded_token = JWT.decode(token, SECRET_KEY, true, {algorithm: "HS256"})
decoded_token[0].as_h? # Return the payload as a hash if verification is successful
rescue JWT::DecodeError => e
puts "Token verification failed: #{e.message}"
nil
end
end
# Example Usage
user_id = 123
token = generate_token(user_id)
puts "Generated token: #{token}"
decoded = verify_token(token)
if decoded
puts "Authenticated User ID: #{decoded["user_id"]}"
else
puts "Authentication failed."
end
"""
**Authorization (Role-Based Access Control)**
"""crystal
enum Role
Admin
Editor
Viewer
end
record User, id : Int32, role : Role
def authorize(user : User, required_role : Role) : Bool
user.role <= required_role
end
# Example Usage
user = User.new(1, Role::Editor)
if authorize(user, Role::Admin)
puts "User is authorized to perform admin actions."
elsif authorize(user, Role::Editor)
puts "User is authorized to perform editor actions."
else
puts "User has insufficient privileges."
end
"""
### Anti-Patterns
* Using predictable or easily guessable passwords.
* Creating custom authentication schemes without rigorous security review.
* Storing sensitive information in cookies without proper encryption and security flags.
* Relying solely on front-end authorization.
## 3. Data Encryption and Storage
### Standard
Encrypt sensitive data at rest and in transit to protect confidentiality.
### Why
Data encryption prevents unauthorized access to sensitive information if systems are compromised.
### Guidelines
* **Do This:**
* Use TLS/SSL for all network communication.
* Encrypt sensitive data at rest using strong encryption algorithms (AES).
* Properly manage encryption keys and secrets.
* Use secure storage solutions for sensitive data.
* **Don't Do This:**
* Transmit sensitive data over unencrypted channels (HTTP).
* Store encryption keys in the code or version control.
* Use weak or outdated encryption algorithms.
### Code Examples
**TLS/SSL (using Kemal)**
"""crystal
require "kemal"
# Kemal framework defaults to TLS/SSL when running in production.
# Ensure proper configuration for SSL certificates (e.g., using Let's Encrypt).
# Configure Kemal to bind to HTTPS port (443 by default) and use your SSL certificates
# In Kemal, ensure SSL certificates are properly configured via environment variables or command-line arguments.
# KEMAL_SSL_CERT=path/to/your/certificate.pem
# KEMAL_SSL_KEY=path/to/your/private_key.pem
get "/" do |env|
"Welcome to my secure site!"
end
"""
**Data Encryption with AES (using "OpenSSL" binding)**
"""crystal
require "openssl"
KEY = "1234567890123456" # a 16-byte key for AES-128
IV = "abcdefghijklmnop" # Initialization Vector (must be 16 bytes for AES)
def encrypt(data : String, key : String, iv : String) : String
cipher = OpenSSL::Cipher.new("aes-128-cbc")
cipher.encrypt
cipher.key = key
cipher.iv = iv
encrypted = cipher.update(data) + cipher.final
Base64.encode64(encrypted)
end
def decrypt(encrypted_data : String, key : String, iv : String) : String
decipher = OpenSSL::Cipher.new("aes-128-cbc")
decipher.decrypt
decipher.key = key
decipher.iv = iv
decoded_data = Base64.decode64(encrypted_data)
decrypted = decipher.update(decoded_data) + decipher.final
decrypted
end
# Example Usage
data = "Sensitive data to encrypt."
encrypted_data = encrypt(data, KEY, IV)
puts "Encrypted data: #{encrypted_data}"
decrypted_data = decrypt(encrypted_data, KEY, IV)
puts "Decrypted data: #{decrypted_data}"
"""
**Secure Secret Management (using environment variables)**
"""crystal
# Do This: Use environment variables to store secrets
db_password = ENV["DATABASE_PASSWORD"]
if db_password.nil?
puts "Error: DATABASE_PASSWORD environment variable not set!"
exit(1)
end
puts "Connecting to database with password from environment."
# Use db_password to connect to the database
# DB.open("postgres://user:#{db_password}@host:port/database") do |db| ... end
# Don't Do This: Hardcoding sensitive information or storing it in version control.
#DB_PASSWORD = "hardcoded_password" # Insecure!
"""
### Anti-Patterns
* Storing encryption keys directly in the code.
* Using weak or outdated encryption algorithms.
* Transmitting sensitive data over unencrypted channels.
* Failing to properly rotate encryption keys.
## 4. Error Handling and Logging
### Standard
Implement proper error handling and logging mechanisms to identify and address security vulnerabilities.
### Why
Detailed logs provide critical information for identifying and investigating security incidents. Careless error handling can leak sensitive information.
### Guidelines
* **Do This:**
* Log all security-related events (authentication failures, authorization violations, etc.).
* Use structured logging formats (JSON) for easier analysis.
* Implement centralized logging and monitoring.
* Sanitize sensitive data before logging.
* Handle exceptions gracefully and avoid revealing sensitive information in error messages.
* **Don't Do This:**
* Log sensitive information (passwords, API keys).
* Use generic error messages that provide limited context.
* Ignore exceptions without logging.
### Code Examples
**Structured Logging**
"""crystal
require "json"
require "time"
def log_event(level : String, message : String, data : Hash(String, String)? = nil)
log_entry = {
"timestamp" => Time.utc.to_s,
"level" => level,
"message" => message,
"data" => data || {}
}
puts log_entry.to_json
end
# Example Usage
log_event("INFO", "User logged in successfully", {"user_id" => "123", "ip_address" => "192.168.1.1"})
log_event("ERROR", "Authentication failed", {"username" => "testuser", "ip_address" => "192.168.1.1"})
# Sanitizing Sensitive Data
sensitive_data = {"credit_card" => "1234-5678-9012-3456"}
sanitized_data = {"credit_card" => "REDACTED"}
log_event("WARN", "Suspicious activity detected", sanitized_data) # Log sanitized data
"""
**Error Handling**
"""crystal
def safe_division(a : Int32, b : Int32) : Int32?
begin
a // b
rescue DivisionByZero
puts "Error: Division by zero!"
log_event("ERROR", "Division by zero attempted", {"value_a" => a.to_s, "value_b" => b.to_s})
nil #Return nil to indicate failure
end
end
# Example Usage
result = safe_division(10, 0)
if result
puts "Result: #{result}"
else
puts "Operation failed."
end
def risky_operation(filename : String)
begin
file = File.open(filename)
puts file.read_all
file.close
rescue e : Errno::ENOENT
log_event("ERROR", "File not found", {"filename" => filename})
puts "File not found. Check logs for details." # User-friendly message
rescue => e
log_event("ERROR", "Generic error: #{e.message}", {"filename" => filename, "error" => e.class.name})
puts "An unexpected error occurred. Check logs for details." # Generic Message.
end
end
risky_operation("non_existent_file.txt") #Example usage.
"""
### Anti-Patterns
* Logging sensitive information in plain text.
* Using generic error messages that provide limited insight.
* Over-logging, which can impact performance and storage.
* Not having a central logging or alerting mechanism.
## 5. Dependencies and Vulnerability Management
### Standard
Regularly update dependencies and scan for vulnerabilities to mitigate risks from third-party libraries.
### Why
Vulnerabilities in dependencies can be exploited to compromise the application.
### Guidelines
* **Do This:**
* Use a dependency management tool (e.g., shards) to manage dependencies.
* Regularly update dependencies to the latest versions.
* Use security scanning tools to identify vulnerabilities in dependencies (e.g., "bundler-audit" equivalent).
* Monitor security advisories for known vulnerabilities.
* **Don't Do This:**
* Use outdated or unsupported dependencies.
* Ignore security advisories for dependencies.
* Fail to regularly scan for vulnerabilities.
### Code Examples
**Using Shards for Dependency Management**
"""yaml
# shard.yml
name: myapp
version: 0.1.0
dependencies:
kemal:
github: kemalcr/kemal
version: "~> 1.4" # Specify desired version. Use "~>" for compatible updates.
jwt:
github: joewandy/crystal-jwt
version: "~> 0.5"
license: MIT
"""
To install dependencies:
"""bash
shards install
"""
To update dependencies to the latest compatible version:
"""bash
shards update
"""
**Vulnerability Scanning Considerations**
Crystal doesn't have a dedicated "bundler-audit" equivalent directly. Here are ways to approach this based on available information and tools. You can use these in combination for best results:
1. **Manual Review and Monitoring:**
* Regularly check for security advisories related to the shards you are using. Monitor the GitHub repositories of your dependencies for reported issues and updates.
* Subscribe to security mailing lists or RSS feeds that announce vulnerabilities in software libraries.
2. **Automated Vulnerability Scanning (Indirectly):**
* **Container Scanning:** If you deploy your Crystal application within a Docker container, use container image scanning tools like Clair, Snyk, or Anchore to identify vulnerabilities in the base OS image and any system-level dependencies.
* **CI/CD Integration:** Integrate security scanning into your CI/CD pipeline by running container scans during the build process. Fail the build if critical vulnerabilities are found.
3. **Shard-Specific Auditing (Future Tooling):**
* The Crystal community may develop shard-specific auditing tools in the future. Keep an eye on community projects and announcements. Such tools would ideally analyze "shard.lock" and compare dependency versions against a known vulnerability database.
**Example - Docker usage for scanning**
"""dockerfile
FROM crystallang/crystal:latest
WORKDIR /app
COPY shard.yml shard.yml
RUN shards install
COPY . .
# Dummy command to make the image scannable, replace with actual build/run execution
CMD ["echo", "Application is built"]
"""
**Then use security scanners like "Trivy" to scan the docker image**
"""bash
docker build -t my-crystal-app .
trivy image my-crystal-app
"""
### Anti-Patterns
* Using outdated dependencies without updates.
* Ignoring security advisories for dependencies.
* Failing to scan for vulnerabilities in dependencies.
* Vendoring dependencies without proper management.
## 6. Denial of Service (DoS) Protection
### Standard
Implement measures to protect against denial-of-service (DoS) and distributed denial-of-service (DDoS) attacks.
### Why
DoS attacks can overwhelm the system and make it unavailable to legitimate users.
### Guidelines
* **Do This:**
* Implement rate limiting to prevent abuse.
* Use a web application firewall (WAF) to filter malicious traffic (Cloudflare, AWS WAF).
* Implement request size limits.
* Use caching to reduce server load.
* **Don't Do This:**
* Ignore potential DoS vectors.
* Fail to monitor system resources for signs of attack.
* Expose unnecessary endpoints.
### Code Examples
**Rate Limiting (using Kemal middleware)**
"""crystal
require "kemal"
require "redis" # or any other caching system
# Simple in-memory rate limiter (for demonstration; use Redis in production)
RATE_LIMITS = {} of String => Int32
def rate_limit(env : HTTP::Env, redis : Redis)
ip_address = env.remote_address.to_s
key = "ratelimit:#{ip_address}"
limit = 100 # requests per minute
expiry = 60 # seconds
# Atomic operation to increment the counter in Redis and get the value
count = redis.incr(key)
# Set expiry only if the key is new
redis.expire(key, expiry) if count == 1
if count > limit
env.response.status_code = 429
env.response.body = "Too Many Requests"
env.response.headers["Retry-After"] = expiry.to_s
return false # Halt the request
else
return true # Proceed with the request
end
end
before do |env|
redis = Redis.new("redis://127.0.0.1:6379") # Replace with your redis connection string.
unless rate_limit(env, redis)
halt # Stop processing the request if rate limit is exceeded
end
end
get "/" do |env|
"Hello, world!"
end
"""
**Request Size Limits (Kemal)**
"""crystal
require "kemal"
#This configuration applies to all routes, can be configured at individual route levels.
configure do |settings|
settings.max_request_size = 10_000_000 # 10MB request limit
end
post "/upload" do |env|
# By default Kemal already protects against large uploads due to the max_request_size configured.
file = env.params.file("myfile")
if file
# Process the uploaded file
puts "File name: #{file.filename}"
puts "File size: #{file.tempfile.size}"
"File uploaded successfully"
else
env.response.status_code = 400
"No file uploaded"
end
end
"""
### Anti-Patterns
* Not implementing any rate limiting.
* Using overly permissive rate limits.
* Failing to monitor for DoS attacks.
* Relying solely on application-level defenses without network-level protection.
## 7. Session Management
### Standard
Implement secure session management practices to protect user sessions from hijacking and unauthorized access.
### Why
Vulnerable session management can lead to user account compromise.
### Guidelines
* **Do This:**
* Use strong, randomly generated session IDs
* Regenerate the session ID after successful login
* Set appropriate session expiration times
* Store session data server-side
* Use secure, HTTP-only cookies
* Implement session timeout and idle timeout
* Validate session data on each request
* **Don't Do This:**
* Use predictable session IDs
* Store sensitive information in session cookies
* Use excessively long session expiration times
* Allow session fixation vulnerabilities.
### Code Examples
**Secure Session Management (using Kemal middleware)**
"""crystal
require "kemal"
require "session"
# configure session settings, use secure cookies, and use suitable secret.
configure do |settings|
settings.session_secret = "A_Very_Long_And_Random_Secret_Key"
settings.session_store = Session::CookieStore.new(
cookie_options: {
secure: true, # Only transmit over HTTPS
httponly: true, # Prevent client-side JavaScript access
same_site: :strict # Helps prevent CSRF attacks
}
)
end
before do |env|
env.session # Accessing env.session initializes the session
end
get "/" do |env|
session = env.session
session["visits"] = (session["visits"] || 0).as(Int32) + 1
"Visits: #{session["visits"]}"
end
get "/login" do |env|
# Simulating authentication successfully after submitted credentials via POST to /login
env.session["user_id"] = 123 # Authenticated user ID
# DO THIS: Regenerate session ID after login to prevent session fixation
env.session.regenerate
redirect "/"
end
get "/logout" do |env|
env.session.clear # Clear all session data
redirect "/"
end
"""
### Anti-Patterns
* Using sequential or predictable session IDs.
* Storing session IDs in the URL.
* Using the same session ID for the entire session lifetime (without regeneration after login).
* Using excessively long session lifetimes.
* Not invalidating sessions on logout.
* Using weak session secrets
## 8. File Uploads
### Standard
Implement secure file upload handling to prevent malicious file uploads and potential security vulnerabilities.
### Why
Unvalidated file uploads can lead to remote code execution, XSS, or denial of service attacks.
### Guidelines
* **Do This:**
* Validate file type based on content, not just the file extension
* Sanitize filenames to prevent directory traversal attacks
* Limit file sizes
* Store uploaded files outside the web root
* Use unique file names to prevent overwrites
* Scan uploaded files for malware
* Set restrictive permissions on uploaded files
* **Don't Do This:**
* Trust the file extension (e.g., "*.jpg", "*.png")
* Store uploaded files directly in the web root
* Allow arbitrary file names
* Execute uploaded files directly
* Use filename filtering as the only means of validation.
### Code Example
"""crystal
require "kemal"
PUBLIC_UPLOAD_DIR = "uploads"
Dir.mkdir_p PUBLIC_UPLOAD_DIR # Create the directory if it doesn't exist
post "/upload" do |env|
file = env.params.file("myfile")
if file
filename = file.filename
tempfile = file.tempfile # This gives you a file-like object
# Validate File type based on content (MIME Type verification) instead of just extension
mime_type = "file --mime-type -b #{tempfile.path}".strip # Requires 'file' utility
allowed_mime_types = ["image/jpeg", "image/png", "image/gif"]
unless allowed_mime_types.includes?(mime_type)
env.response.status_code = 400
puts "Invalid file type: #{mime_type}"
"Invalid file type."
next
end
#Sanitize filename to prevent path traversal.
sanitized_filename = filename.gsub(%r{[^a-zA-Z0-9._-]}) { "" } # Remove any characters that aren't letters, numbers, underscores, periods, or hyphens
safe_filename = File.join(PUBLIC_UPLOAD_DIR, "#{Time.utc.to_unix}_#{sanitized_filename}")
#Limit file uploads size.
max_file_size = 10_000_000 #10MB
if file.tempfile.size > max_file_size
"File is too large"
env.response.status_code = 413
next
end
# Save File
File.copy(file.tempfile.path, safe_filename)
puts "File uploaded to: #{safe_filename}"
"File uploaded successfully."
else
env.response.status_code = 400
"No file uploaded"
end
end
"""
### Anti-Patterns
* Trusting client-provided filenames or MIME types without server-side validation.
* Storing uploaded files in the webserver's document root without protecting them.
* Allowing unrestricted file sizes or file types.
* Not sanitizing uploaded filenames.
* Attempting to execute uploaded files directly.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Crystal This document outlines component design standards for Crystal projects, aiming to foster reusable, maintainable, and performant code. These standards are tailored specifically for Crystal's features and ecosystem. ## 1. Component Architecture A component is a self-contained, reusable unit of functionality with a well-defined interface. In Crystal, components can be represented by modules, classes, structs, or even standalone functions. The overall application architecture should be component-based to promote modularity. ### 1.1. Loose Coupling * **Do This:** Design components with minimal knowledge of each other. Use interfaces or abstract classes to communicate. * **Don't Do This:** Create tight dependencies between components, making them difficult to change or reuse independently. * **Why:** Loose coupling enhances modularity and flexibility. Changes in one component are less likely to affect others. """crystal # Good: Using an interface abstract class PaymentProcessor abstract def process_payment(amount : Float64) end class StripeProcessor < PaymentProcessor def process_payment(amount : Float64) puts "Processing #{amount} with Stripe" end end class PaypalProcessor < PaymentProcessor def process_payment(amount : Float64) puts "Processing #{amount} with Paypal" end end # The client depends on the abstract PaymentProcessor, not the concrete implementations class Order def initialize(@payment_processor : PaymentProcessor) end def checkout(amount : Float64) @payment_processor.process_payment(amount) end end stripe = StripeProcessor.new order = Order.new(stripe) order.checkout(100.0) """ """crystal # Bad: Tightly Coupled class StripePayment def process(amount : Float64) puts "Processing payment using Stripe: #{amount}" end end class OrderProcessor def process_order(amount : Float64) stripe = StripePayment.new stripe.process(amount) # Tight coupling - OrderProcessor knows too much about StripePayment end end order_processor = OrderProcessor.new order_processor.process_order(50.0) """ ### 1.2. High Cohesion * **Do This:** Ensure each component encapsulates a single, well-defined purpose. * **Don't Do This:** Create God classes or modules that try to handle too many unrelated responsibilities. * **Why:** High cohesion leads to simpler, more understandable code. It simplifies maintenance and testing. """crystal # Good: High Cohesion module UserAuthentication def self.authenticate(username : String, password : String) : Bool # ... authentication logic ... true # Stubbed for example end def self.authorize(user_id : Int32, resource : String) : Bool # ... authorization logic ... true # Stubbed for example end end # The module is focused solely on user authentication and authorization """ """crystal # Bad: Low Cohesion module UserManagement def self.authenticate(username : String, password : String) : Bool # ... authentication logic ... true # Stubbed for example end def self.create_user(username : String, email : String) # ... user creation logic ... end def self.send_welcome_email(email : String) # ... send welcome email logic ... end end # The module attempts to handle authentication, user creation, and email sending - low cohesion! """ ### 1.3. Explicit Interfaces * **Do This:** Define clear interfaces for each component using abstract classes or modules with defined public methods. * **Don't Do This:** Rely on implicit interfaces or expose internal implementation details. * **Why:** Explicit interfaces promote predictability and allow for easier substitution of components. """crystal # Good: Interface definition using abstract class abstract class Storage abstract def save(key : String, value : String) abstract def load(key : String) : String? # String? to indicate nullable return abstract def delete(key : String) end class FileStorage < Storage def save(key : String, value : String) File.write(key, value) end def load(key : String) : String? File.read(key) rescue nil end def delete(key : String) File.delete(key) end end # Usage: storage = FileStorage.new storage.save("my_key", "my_value") puts storage.load("my_key") """ """crystal # Bad: Implicit Interface class DataWriter def write_data(data) # Implementation implies it takes any data puts "Writing data: #{data}" end end class DataProcessor def process(writer) writer.write_data({"key" => "value"}) # Assumes DataWriter has write_data, but no explicit contract. end end """ ## 2. Component Implementation ### 2.1. Use Crystal Generics Judiciously * **Do This:** Use generics to create reusable components that work with different types. * **Don't Do This:** Overuse generics, leading to complex and unreadable code. Consider type constraints. * **Why:** Generics enhance code reuse while maintaining type safety. """crystal # Good: Using Generics with Type Constraints class Cache(T) getter data : Hash(String, T) def initialize @data = Hash(String, T).new end def put(key : String, value : T) @data[key] = value end def get(key : String) : T? @data[key] end end cache_string = Cache(String).new cache_string.put("name", "Crystal") puts cache_string.get("name") cache_int = Cache(Int32).new cache_int.put("port", 8080) puts cache_int.get("port") """ """crystal # Bad: Overusing Generics class GenericProcessor(T, U, V) # Too many generic types, hard to reason about def process(input1 : T, input2 : U) : V # Complex logic involving T, U, and V... input1.to_s + input2.to_s # Just an example end end """ ### 2.2. Favor Composition over Inheritance * **Do This:** Build complex components by combining simpler components rather than relying heavily on inheritance. * **Don't Do This:** Create deep inheritance hierarchies that become rigid and difficult to maintain. * **Why:** Composition provides better flexibility and avoids the problems associated with the fragile base class problem. """crystal # Good: Composition example class Logger def log(message : String) puts "Log: #{message}" end end class StatsCollector def collect_stats(data) puts "Collecting Stats: #{data}" end end class Service def initialize(@logger : Logger, @stats : StatsCollector) end def run(data) @logger.log("Starting service") @stats.collect_stats(data) puts "Service running with #{data}" @logger.log("Service completed") end end logger = Logger.new stats = StatsCollector.new service = Service.new(logger, stats) service.run({"requests" => 100}) """ """crystal # Bad: Deep Inheritance class BaseComponent def initialize puts "Base Initialized" end end class ExtendedComponent < BaseComponent def initialize super # Always remember to call super! puts "Extended initialized" end end class EvenMoreExtended < ExtendedComponent def initialize super # Remember to call super! puts "EvenMoreExtended Initialized" end end component = EvenMoreExtended.new # Difficult to trace initialization flow. Inheritance makes debugging harder. """ ### 2.3. Use Modules for Namespaces and Utility Functions * **Do This:** Group related functions and constants within modules to avoid naming conflicts and organize code. * **Don't Do This:** Define global functions and constants that pollute the namespace. Modules offer a better approach. * **Why:** Modules create clear boundaries and improve code organization. """crystal # Good: Module Usage module StringUtils def self.reverse(str : String) : String str.reverse end def self.capitalize(str : String) : String str.capitalize end end puts StringUtils.reverse("hello") # olleh puts StringUtils.capitalize("world") # World """ """crystal # Bad: Global functions def reverse_string(str : String) : String str.reverse end def capitalize_string(str : String) : String str.capitalize end puts reverse_string("hello") puts capitalize_string("world") # pollute the global namespace """ ### 2.4. Immutability Where Possible * **Do This:** Favor immutable data structures and components to avoid unexpected side effects. * **Don't Do This:** Mutate data structures directly whenever possible. * **Why:** Immutability simplifies reasoning about code and improves thread safety. """crystal # Good: Immutability record Point, x : Int32, y : Int32 point1 = Point.new(1, 2) point2 = Point.new(point1.x + 1, point1.y + 1) # Creating new instance, not mutating puts point1 puts point2 """ """crystal # Bad: Mutable State class MutableCounter property count : Int32 def initialize @count = 0 end def increment @count += 1 # Mutates the state directly end end counter = MutableCounter.new counter.increment puts counter.count """ ### 2.5. Error Handling Strategy * **Do This:** Employ exceptions and "Result" types to deal with errors gracefully. * **Don't Do This:** Ignore errors or rely on return codes without proper handling. * **Why:** Robust error handling prevents application crashes and allows for graceful recovery. """crystal # Good: Using "Result" type for error handling require "option_parser" def safe_divide(a : Float64, b : Float64) : Result(Float64, String) if b == 0.0 return Error("Cannot divide by zero") else return Ok(a / b) end end result = safe_divide(10.0, 2.0) if result.success? puts "Result: #{result.value}" else puts "Error: #{result.error}" end result = safe_divide(5.0, 0.0) if result.success? puts "Result: #{result.value}" else puts "Error: #{result.error}" end """ """crystal # Bad: Ignoring Errors def divide(a : Float64, b : Float64) : Float64 a / b # No handling of potential division by zero end puts divide(10.0, 0.0) # Will crash the program """ ## 3. Specific Design Patterns for Crystal ### 3.1. Strategy Pattern * **How:** Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. """crystal # Strategy Interface abstract class CompressionStrategy abstract def compress(file_path : String) end # Concrete Strategies class ZipCompression < CompressionStrategy def compress(file_path : String) puts "Compressing #{file_path} using ZIP" end end class GzipCompression < CompressionStrategy def compress(file_path : String) puts "Compressing #{file_path} using GZIP" end end # Context class FileCompressor def initialize(@strategy : CompressionStrategy) end def compress_file(file_path : String) @strategy.compress(file_path) end end # Client Code zip_strategy = ZipCompression.new gzip_strategy = GzipCompression.new compressor = FileCompressor.new(zip_strategy) compressor.compress_file("my_file.txt") compressor = FileCompressor.new(gzip_strategy) compressor.compress_file("another_file.txt") """ ### 3.2. Observer Pattern * **How:** Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. """crystal # Subject interface class Subject def initialize @observers = [] of Observer end def attach(observer : Observer) @observers << observer end def detach(observer : Observer) @observers.delete(observer) end def notify @observers.each { |observer| observer.update(self) } end end # Concrete Subject class NewsPublisher < Subject getter news : String def news=(news : String) @news = news notify # Notify after state change end end # Observer Interface abstract class Observer abstract def update(subject : Subject) end # Concrete Observers class EmailSubscriber < Observer def update(subject : Subject) puts "Email Subscriber received news: #{subject.news}" end end class SMSSubscriber < Observer def update(subject : Subject) puts "SMS Subscriber received news: #{subject.news}" end end # Client Code publisher = NewsPublisher.new email_subscriber = EmailSubscriber.new sms_subscriber = SMSSubscriber.new publisher.attach(email_subscriber) publisher.attach(sms_subscriber) publisher.news = "Crystal 1.7 released!" """ ### 3.3. Factory Pattern * **How:** Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses. """crystal # Abstract Product abstract class Document abstract def open end # Concrete Products class PDFDocument < Document def open puts "Opening PDF document" end end class TextDocument < Document def open puts "Opening Text document" end end # Abstract Factory abstract class DocumentFactory abstract def create_document : Document end # Concrete Factories class PDFFactory < DocumentFactory def create_document : Document PDFDocument.new end end class TextFactory < DocumentFactory def create_document : Document TextDocument.new end end # Client Code pdf_factory = PDFFactory.new pdf_document = pdf_factory.create_document pdf_document.open text_factory = TextFactory.new text_document = text_factory.create_document text_document.open """ ## 4. Code Style and Formatting * **Use "crystal tool format":** Consistently format code using the built-in formatter. * **Follow Naming Conventions:** class "PascalCase", method "snake_case". * **Limit Line Length:** Aim for a maximum of 80-120 characters per line. * **Add Comments:** Clear and concise comments to explain complex logic. Use proper documentation for public APIs. ## 5. Conclusion Adhering to these component design standards will significantly improve the quality, maintainability, and reusability of Crystal codebases. This document will serve as a guide for new developers and a reference for maintaining consistent code quality across projects.
# Code Style and Conventions Standards for Crystal This document outlines the code style and conventions standards for the Crystal programming language. Adhering to these guidelines will promote code consistency, readability, and maintainability, and contribute significantly to collaborative development efforts. These guidelines are designed to align with the latest version of Crystal and leverage best practices in the Crystal ecosystem. The intention of this document is to also act as a contextual document that can be used in tandem with AI coding assistants such as GitHub Copilot. ## 1. General Formatting ### 1.1. Whitespace * **Do This:** Use 2 spaces for indentation. Avoid tabs. * **Don't Do This:** Use tabs or more/
# Core Architecture Standards for Crystal This document outlines the core architectural standards for Crystal projects. It aims to provide clear guidelines for project structure, architectural patterns, and organization principles, specifically tailored for Crystal. Following these standards will promote maintainability, performance, security, and overall code quality. These standards use Crystal version "1.11.2". ## 1. Project Structure and Organization ### 1.1 General Structure * **Do This:** Adhere to a consistent and predictable project structure. A recommended structure is: """ my_project/ ├── src/ # Source code │ ├── my_project/ # Module directory │ │ ├── models/ # Data models │ │ │ ├── user.cr │ │ │ └── product.cr │ │ ├── controllers/ # Request Handlers │ │ │ ├── user_controller.cr │ │ │ └── product_controller.cr │ │ ├── services/ # Business Logic │ │ │ ├── user_service.cr │ │ │ └── product_service.cr │ │ ├── repositories/ # Data Access Layer │ │ │ ├── user_repository.cr │ │ │ └── product_repository.cr │ │ ├── validators/ # Data validation logic │ │ │ ├── user_validator.cr │ │ │ └── product_validator.cr │ │ ├── utils/ # Supporting utilities │ │ │ ├── string_utils.cr │ │ │ └── date_utils.cr │ │ ├── application.cr # Main application logic │ │ └── web.cr # Web framework initialization │ ├── main.cr # Entry point ├── spec/ # Tests │ ├── spec_helper.cr # Test setup │ ├── models/ │ │ ├── user_spec.cr │ │ └── product_spec.cr │ ├── controllers/ │ │ ├── user_controller_spec.cr │ │ └── product_controller_spec.cr ├── shards.yml # Dependency management ├── .gitignore # Git ignore file └── README.md # Project overview """ * **Don't Do This:** Scatter code arbitrarily throughout the project or create excessively deep directory structures. * **Why:** A well-defined project structure enhances navigability, understandability, and maintainability. It promotes separation of concerns and makes it easier for developers to locate specific components. ### 1.2 Module Organization * **Do This:** Group related code into modules that reflect logical components (e.g., "MyProject::Users", "MyProject::Products"). """crystal module MyProject module Users class User # ... end end module Products class Product # ... end end end """ * **Don't Do This:** Place all code in a single, monolithic module, or create modules that don't represent meaningful groupings. * **Why:** Modules provide namespaces and prevent naming conflicts. They also make it easier to manage code dependencies. ### 1.3 Layered Architecture * **Do This:** Implement a layered architecture. Each layer has a specific responsibility. Common layers include: * **Presentation Layer (Controllers):** Handles user interaction and request/response cycles. * **Application Layer (Services):** Orchestrates business logic. * **Domain Layer (Models):** Represents core business entities and logic. * **Infrastructure Layer (Repositories):** Manages data persistence and external integrations. * **Don't Do This:** Allow layers to directly depend on other layers that are not immediately adjacent (e.g., controller directly accessing a database repository). * **Why:** Layered architectures promote separation of concerns and improve testability. Changes in one layer are less likely to impact other layers. """crystal # src/my_project/controllers/user_controller.cr require "./services/user_service" module MyProject module Controllers class UserController def initialize(@user_service : Services::UserService) end def create(params : Hash(String, String)) : HTTP::Response begin user = @user_service.create(params) HTTP::Response.ok(user.to_json) rescue => e HTTP::Response.bad_request(e.message) end end end end end # src/my_project/services/user_service.cr require "./repositories/user_repository" require "./validators/user_validator" module MyProject module Services class UserService def initialize(@user_repository : Repositories::UserRepository, @user_validator : Validators::UserValidator) end def create(params : Hash(String, String)) : Users::User user = Users::User.new(username: params["username"], email: params["email"]) if @user_validator.valid?(user) @user_repository.create(user) else raise "Invalid user data" end end end end end # src/my_project/repositories/user_repository.cr require "./models/user" require "sqlite3" module MyProject module Repositories class UserRepository def initialize(@db : SQLite3::Database = SQLite3::Database.new("my_database.db")) @db.execute "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, email TEXT)" end def create(user : Users::User) : Users::User statement = @db.prepare "INSERT INTO users (username, email) VALUES (?, ?)" statement.bind(1, user.username) statement.bind(2, user.email) statement.execute user end end end end # src/my_project/models/user.cr require "json" module MyProject module Users class User include JSON::Serializable property id : Int32? property username : String property email : String def initialize(username : String, email : String, id : Int32? = nil) @id = id @username = username @email = email end end end end # src/my_project/validators/user_validator.cr module MyProject module Validators class UserValidator def valid?(user : Users::User) : Bool !user.username.empty? && user.email.includes?("@") end end end end """ ## 2. Design Patterns ### 2.1 Dependency Injection * **Do This:** Use dependency injection through constructor injection and setter injection. """crystal # Constructor injection class MyClass def initialize(@dependency : Dependency) end def do_something @dependency.some_method end end # Setter injection class AnotherClass property dependency : Dependency def set_dependency(dependency : Dependency) @dependency = dependency end def do_something_else @dependency.another_method end end """ * **Don't Do This:** Create tight coupling between classes by directly instantiating dependencies within a class. * **Why:** Dependency injection promotes loose coupling, which enhances testability and flexibility because a mock or stub dependency can easily replace it in a test. Dependencies become explicit, improving code readability. ### 2.2 Factory Pattern * **Do This:** Use a factory pattern to encapsulate the logic of object creation, particularly when the creation process is complex or involves multiple steps. """crystal abstract class PaymentGateway abstract def process_payment(amount : Float) : Bool end class CreditCardGateway < PaymentGateway def process_payment(amount : Float) : Bool puts "Processing credit card payment: #{amount}" true # Simulated successful payment end end class PayPalGateway < PaymentGateway def process_payment(amount : Float) : Bool puts "Processing PayPal payment: #{amount}" true # Simulated successful payment end end class PaymentGatewayFactory def self.create_gateway(type : String) : PaymentGateway case type when "credit_card" CreditCardGateway.new when "paypal" PayPalGateway.new else raise "Invalid payment gateway type: #{type}" end end end # Usage gateway = PaymentGatewayFactory.create_gateway("paypal") gateway.process_payment(100.50) # => Processing PayPal payment: 100.5 """ * **Don't Do This:** Embed object creation logic directly within client classes, especially when dealing with conditional object instantiation. * **Why:** The factory pattern decouples client code from the object creation process, allowing for greater flexibility and maintainability. It centralizes object creation logic, making it easier to modify or extend. ### 2.3 Observer Pattern * **Do This:** Implement the observer pattern when you need to notify multiple dependent objects (observers) about changes to a subject object. """crystal class Subject getter observers : Array(Observer) = [] of Observer def attach(observer : Observer) @observers << observer end def detach(observer : Observer) @observers.delete(observer) end def notify @observers.each { |observer| observer.update(self) } end end abstract class Observer abstract def update(subject : Subject) end class ConcreteObserver < Observer def initialize(@name : String) end def update(subject : Subject) puts "#{@name} received update from subject" end end # Usage subject = Subject.new observer1 = ConcreteObserver.new("Observer 1") observer2 = ConcreteObserver.new("Observer 2") subject.attach(observer1) subject.attach(observer2) subject.notify # => Observer 1 received update from subject # => Observer 2 received update from subject subject.detach(observer1) subject.notify # => Observer 2 received update from subject """ * **Don't Do This:** Create tight dependencies between subject and observer classes where the subject is required to know about the concrete types of the observers. * **Why:** The observer pattern establishes a one-to-many dependency between objects without tightly coupling them. This simplifies and improves flexibility by allowing components to react to changes without the subject needing to be aware of their implementation. ## 3. Concurrency and Parallelism ### 3.1 Fibers for Concurrency * **Do This:** Use "Fiber" for I/O-bound concurrency tasks to prevent blocking the main thread, improving responsiveness. """crystal require "socket" def handle_connection(client : TCPSocket) begin puts "Handling client: #{client.remote_address}" loop do data = client.gets break unless data puts "Received: #{data.strip} from #{client.remote_address}" client.puts "Echo: #{data.strip}" end ensure puts "Closing connection with #{client.remote_address}" client.close end end server = TCPServer.new("127.0.0.1", 3000) puts "Server listening on port 3000" loop do client = server.accept Fiber.new { handle_connection(client) }.resume # start each connection in its own Fiber end """ * **Don't Do This:** Perform long-running I/O operations directly in the main thread, blocking other operations. * **Why:** "Fiber" provides lightweight concurrency, allowing the application to handle multiple tasks concurrently without the overhead of threads. ### 3.2 Parallelism with "spawn" * **Do This:** Utilize "spawn" for CPU-bound tasks to leverage multiple cores and achieve parallelism. Use "Channel" to communicate and synchronize data between spawned processes. """crystal require "concurrent" def process_data(data : Array(Int32)) : Int32 sum = 0 data.each { |x| sum += x } sum end data = (1..1000).to_a.map(&.to_i32) chunk_size = 250 chunks = data.each_slice(chunk_size).to_a channel = Concurrent::Channel.new chunks.each do |chunk| spawn do result = process_data(chunk) channel.send(result) end end total = 0 chunks.size.times do total += channel.receive end puts "Total: #{total}" """ * **Don't Do This:** Overuse "spawn" for simple tasks, which can introduce unnecessary overhead. * **Why:** "spawn" allows executing code in parallel on multiple cores, resulting in faster processing. ### 3.3 Channels for Communication * **Do This:** Employ "Channel" for safe communication and synchronization between fibers or processes. """crystal channel = Channel(String).new spawn do channel.send("Hello from Fiber 1") end spawn do message = channel.receive puts "Fiber 2 received: #{message}" end """ * **Don't Do This:** Share mutable state directly between concurrent entities without proper synchronization mechanisms, leading to race conditions and data corruption. * **Why:** Channels provide a safe and reliable way to exchange data between concurrent entities, preventing data races and ensuring that data is accessed in a synchronized manner. ## 4. Error Handling ### 4.1 Explicit Error Handling * **Do This:** Handle errors explicitly using "begin...rescue...end" blocks to catch and manage exceptions. """crystal begin # Code that may raise an exception result = 10 / 0 puts "Result: #{result}" # This line will not be reached if an exception occurs rescue ex : Exception puts "Error: #{ex.message}" end """ * **Don't Do This:** Ignore potential errors or rely on default exception handling, which can lead to unexpected behavior. * **Why:** Explicit error handling ensures that your application can gracefully recover from errors and provide meaningful feedback to the user or administrator. ### 4.2 Custom Exceptions * **Do This:** Define and use custom exception classes to provide more specific error information. """crystal class AuthenticationError < Exception end def authenticate(username, password) raise AuthenticationError.new("Invalid username or password") unless username == "admin" && password == "secret" true end begin authenticate("user", "wrong_password") rescue ex : AuthenticationError puts "Authentication failed: #{ex.message}" end """ * **Don't Do This:** Use generic "Exception" classes for all errors, limiting the ability to handle specific error types. * **Why:** Custom exceptions allow for more granular error handling and provide better context for debugging. ### 4.3 Logging Errors * **Do This:** Log errors with sufficient detail (timestamp, error message, stack trace) to help diagnose and resolve issues. """crystal require "logger" $log = Logger.new(STDOUT) $log.level = Logger::INFO begin # Code that may raise an exception result = 10 / 0 puts "Result: #{result}" # This line will not be reached if an exception occurs rescue ex : Exception $log.error "Error: #{ex.message}" $log.error ex.backtrace.join("\n") end """ * **Don't Do This:** Omit error logging or log insufficient information, making it difficult to troubleshoot problems. * **Why:** Comprehensive error logging is crucial for identifying and resolving issues in a production environment. ## 5. Data Handling and Persistence ### 5.1 ORM Usage * **Do This:** Use an ORM like "Granite" or roll your own data mapping layer for database interactions to abstract away raw SQL. This enables easier switching of databases and improves readability. """crystal # Example using Granite (assuming setup already done) require "granite" # Model definition class User < Granite::Base column name : String column email : String end # Create a new user user = User.new(name: "John Doe", email: "john@example.com") user.save # Fetch a user retrieved_user = User.find(user.id) puts retrieved_user.name # Output: John Doe """ * **Don't Do This:** Embed raw SQL queries directly within application logic, making maintenance complex and increasing the risk of SQL injection vulnerabilities. * **Why:** ORMs provide a higher level of abstraction for data access, simplifying database operations, preventing SQL injection vulnerabilities, and improving code readability. ### 5.2 Data Validation * **Do This:** Implement data validation to ensure data integrity and prevent invalid data from being persisted. """crystal class User property name : String property email : String def valid? !name.empty? && email.includes?("@") end end user = User.new(name: "", email: "invalid-email") if user.valid? puts "User is valid" else puts "User is invalid" # Output: User is invalid end """ * **Don't Do This:** Trust user input or external data sources without validation and sanitation. * **Why:** Data validation prevents common errors and security vulnerabilities by ensuring that only valid data is processed and stored. ### 5.3 Database Connection Management * **Do This:** Use a connection pool to efficiently manage database connections. This is especially important for web applications to handle concurrent requests. Implement the pool using a library like "dbcp". """crystal require "dbcp" require "sqlite3" pool = DBCP::Pool.new(SQLite3::Database, "mydb.db", max_size: 10) pool.acquire do |db| db.execute "SELECT * FROM users" do |result| puts result.inspect end end """ * **Don't Do This:** Create and close database connections for each operation which creates unneeded overhead. * **Why:** Connection pooling reuses database connections and improves performance. ## 6. Security ### 6.1 Input Sanitization * **Do This:** Sanitize all user inputs to prevent cross-site scripting (XSS) and other injection attacks. """crystal require "cgi" def sanitize(input : String) : String CGI.escape_html(input) end unsanitized_input = "<script>alert('XSS')</script>Hello" sanitized_input = sanitize(unsanitized_input) puts sanitized_input # Output: <script>alert('XSS')</script>Hello """ * **Don't Do This:** Output user-provided data directly without sanitization, potentially exposing your application to security vulnerabilities. * **Why:** Input sanitization transforms malicious input into benign output, preventing XSS and other injection attacks. ### 6.2 Authentication and Authorization * **Do This:** Implement robust authentication and authorization mechanisms to protect sensitive data and functionality. Use established libraries like "Kemal", "Lucky" or devise your own solution with bcrypt for password hashing. """crystal require "bcrypt" def hash_password(password : String) : String BCrypt::Password.create(password) end def verify_password(password : String, hash : String) : Bool BCrypt::Password.new(hash) == password end hashed_password = hash_password("my_secret_password") puts "Hashed password: #{hashed_password}" is_valid = verify_password("my_secret_password", hashed_password) puts "Password is valid: #{is_valid}" # Output: Password is valid: true """ * **Don't Do This:** Use weak or nonexistent authentication and authorization, or hardcode credentials in the application. * **Why:** Robust authentication ensures that only authorized users can access sensitive resources. ### 6.3 HTTPS * **Do This:** Enforce HTTPS to encrypt all communication between the client and server. * **Don't Do This:** Transmit sensitive data over unencrypted HTTP connections. * **Why:** HTTPS protects data in transit from eavesdropping and tampering. ### 6.4 CSRF Protection * **Do This:** Implement CSRF (Cross-Site Request Forgery) protection for all state-changing requests through hidden tokens and Origin/Referer header validation. * **Don't Do This:** Allow all requests without CSRF validation because attackers could create malicious requests on behalf of other users. * **Why:** CSRF protection prevents attackers from forcing authenticated users to perform actions without their knowledge or consent. ## 7. Code Style and Formatting ### 7.1 Naming Conventions * **Do This:** Follow consistent naming conventions. Use "snake_case" for variables, methods, and files. Use "PascalCase" for classes and modules. Constants should use "UPPER_SNAKE_CASE". """crystal module MyModule class MyClass MY_CONSTANT = "constant_value" def my_method(my_variable : Int32) local_variable = my_variable * 2 puts local_variable end end end """ * **Don't Do This:** Use inconsistent or unclear naming conventions, making code harder to understand. * **Why:** Consistent naming improves code readability and maintainability. ### 7.2 Code Comments * **Do This:** Write clear and concise code comments to explain complex logic or non-obvious behavior. Use YARD-style comments for documenting APIs. """crystal # This method calculates the area of a rectangle. # # Args: # width (Float): The width of the rectangle. # height (Float): The height of the rectangle. # # Returns: # Float: The area of the rectangle. def calculate_area(width : Float, height : Float) : Float width * height end """ * **Don't Do This:** Write excessive or redundant comments that state the obvious or are outdated. * **Why:** Meaningful comments enhance code understandability and maintainability. ### 7.3 Line Length * **Do This:** Limit line length to 80-120 characters to improve readability. * **Don't Do This:** Write excessively long lines that require horizontal scrolling. * **Why:** Shorter lines are easier to read and prevent code from becoming visually overwhelming. ### 7.4 Whitespace * **Do This:** Use consistent whitespace for indentation, spacing around operators, and blank lines to separate logical code blocks. """crystal def my_method(arg1 : Int32, arg2 : String) if arg1 > 10 puts "arg1 is greater than 10" else puts "arg1 is not greater than 10" end end """ * **Don't Do This:** Use inconsistent whitespace, making code harder to read. * **Why:** Consistent whitespace makes code visually appealing and easier to understand. This document provides a foundation for writing high-quality Crystal code by establishing clear architectural guidelines, enforcing consistent formatting, and emphasizing security considerations. By adhering to these standards, development teams can greatly improve the maintainability, performance, and overall quality of their Crystal projects.
# State Management Standards for Crystal This document outlines the coding standards for state management in Crystal projects. It aims to provide guidance on how to effectively manage application state, data flow, and reactivity, ensuring maintainability, performance, and security. These standards should be followed by all developers contributing to Crystal projects and serve as context for AI coding assistants. ## 1. Principles of State Management in Crystal ### 1.1. Understanding State State encompasses all the data that defines the current condition or situation of an application at any given time. Managing this state accurately and efficiently is crucial for the correct behavior of any application. * **Why it Matters:** Poorly managed state can lead to unpredictable behavior, bugs that are hard to track, and scalability issues. ### 1.2. Local vs. Global State * **Local State:** State confined to a specific component or module. It is typically simpler to manage and has less impact on the rest of the application. * **Global State:** State that is accessible and modifiable from anywhere in the application. Requires careful management to avoid conflicts and performance bottlenecks. ### 1.3. Immutability vs. Mutability * **Immutability:** Once an object is created, its state cannot be changed. Promotes predictability and simplifies debugging. * **Mutability:** The state of an object can be changed after creation. Requires careful synchronization to avoid race conditions and data corruption in concurrent environments. ## 2. Managing Global Application State ### 2.1. The Singleton Pattern (Use Sparingly) The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. While useful in certain scenarios, overuse can lead to tightly coupled code and difficulties in testing. In Crystal, this is very easy to implement. * **Do This:** Use sparingly for truly global resources like configuration managers or logging services. * **Don't Do This:** Avoid using Singletons for business logic or data storage. **Example:** """crystal class Configuration private @@instance : Configuration? getter setting1 : String getter setting2 : Int32 private def initialize # Load configuration from file or environment @setting1 = "Default Value" @setting2 = 42 end def self.instance : Configuration @@instance ||= new end end # Usage config = Configuration.instance puts config.setting1 # Output: Default Value """ * **Why it Matters:** Controls access to a shared resource and prevents multiple instances. However, Singletons can make code harder to test and reason about. ### 2.2. Dependency Injection (Recommended) Dependency Injection (DI) provides dependencies to a component rather than having the component create or obtain them itself. This promotes loose coupling, testability, and reusability. * **Do This:** Favor DI for managing global state dependencies, especially in larger applications. Use constructor injection, method injection, or property injection. * **Don't Do This:** Avoid hardcoding dependencies within classes. * **Consider This:** The "lucky_di" shard is common for DI in Crystal. However, often simple manual injection is sufficient and preferred due to the simplicity of Crystal's object model. **Example:** """crystal class DatabaseConnection def query(sql : String) : Array(Hash(String, String)) # Simulate a database query puts "Executing query: #{sql}" [{"id" => "1", "name" => "Example"}] end end class UserRepository getter db_connection : DatabaseConnection def initialize(@db_connection : DatabaseConnection) end def get_user(id : Int32) : Hash(String, String)? results = @db_connection.query("SELECT * FROM users WHERE id = #{id}") results.first end end # Usage db = DatabaseConnection.new user_repo = UserRepository.new(db) user = user_repo.get_user(1) puts user # Output: Some({"id" => "1", "name" => "Example"}) """ * **Why it Matters:** Reduces coupling, improves testability, and makes it easier to swap out dependencies. ### 2.3. Configuration Objects Centralizing configuration parameters into configuration objects provides a convenient and organized way to manage application settings. * **Do This:** Load configuration from environment variables, configuration files (e.g., YAML, JSON), or command-line arguments during application startup. Use "ENV" for simple environment variable access. Consider shards like "config" or "kemal-config" for loading from files. * **Don't Do This:** Hardcoding configuration values directly in the code. **Example:** """crystal require "yaml" struct DatabaseConfig property host : String property port : Int32 property username : String property password : String end struct AppConfig property database : DatabaseConfig property log_level : String end def load_config(file_path : String) : AppConfig yaml = YAML.parse_file(file_path) database_config = DatabaseConfig.new( host: yaml["database"]["host"].as_s, port: yaml["database"]["port"].as_i, username: yaml["database"]["username"].as_s, password: yaml["database"]["password"].as_s ) AppConfig.new( database: database_config, log_level: yaml["log_level"].as_s ) end # Usage config = load_config("config.yml") puts config.database.host # Output: localhost puts config.log_level # Output: debug """ * **Why it Matters:** Simplifies configuration management and allows for easy modification without changing the code. ## 3. Managing Local Component State ### 3.1. Explicit State Variables For simple component state, use explicit instance variables to store and manage the state within the component. * **Do This:** Declare instance variables to hold the necessary state. * **Don't Do This:** Rely on implicit state or complex data structures for simple state management. **Example:** """crystal class Counter getter count : Int32 def initialize(@count : Int32 = 0) end def increment @count += 1 end def decrement @count -= 1 end end # Usage counter = Counter.new counter.increment puts counter.count # Output: 1 counter.decrement puts counter.count # Output: 0 """ * **Why it Matters:** Provides clear visibility and control over component state. ### 3.2. State Machines For more complex component state, consider using a state machine to manage the different states and transitions between them. * **Do This:** Define states, events, and transitions explicitly. Consider using a dedicated library for state machine management (no widely adopted standard lib exists, requires custom implementation or very small shard). * **Don't Do This:** Managing complex state transitions with nested "if" statements or ad-hoc logic. **Example:** """crystal enum State Idle Loading Loaded Error end class DataFetcher getter state : State def initialize @state = State::Idle end def load_data @state = State::Loading begin # Simulate loading data sleep 1 @state = State::Loaded rescue => e puts "Error: #{e}" @state = State::Error end end def process_data case @state when State::Loaded puts "Processing data..." else puts "Data not loaded yet." end end end # Usage fetcher = DataFetcher.new puts fetcher.state # Output: Idle fetcher.load_data puts fetcher.state # Output: Loaded fetcher.process_data # Output: Processing data... """ * **Why it Matters:** Formalizes state transitions, making the code more predictable, maintainable, and easier to test. ### 3.3. Immutability and State Updates Favor immutability when possible to simplify state management and prevent unintended side effects. Use immutable data structures and functions that return new state rather than modifying existing state. Crystal's value types (Int, Float, Tuple, etc.) are immutable. * **Do This:** Use immutable data structures. When updating state, create a new object with the modified state. **Example:** """crystal record Point, x : Int32, y : Int32 do def move(dx : Int32, dy : Int32) : Point Point.new(x + dx, y + dy) end end # Usage p1 = Point.new(x: 1, y: 2) p2 = p1.move(3, 4) puts p1 # Output: Point(x: 1, y: 2) puts p2 # Output: Point(x: 4, y: 6) """ * **Why it Matters:** Reduces the risk of bugs caused by unexpected state changes and simplifies reasoning about the code. ### 3.4 Thread Safety Crystal is inherently thread-safe due to its enforced isolation between threads using channels. When sharing state you *must* explicitly share it via channels; shared memory with concurrent access is disallowed by the compiler, preventing many common data race conditions. * **Do This:** Make use of channels to communicate between threads and share access to resources. Favor sending copies of data over references in concurrent contexts. * **Don't Do This:** Directly share mutable state across threads without synchronization mechanisms. **Example:** """crystal require "concurrent" channel = Channel(Int32).new spawn do 10.times do |i| channel.send(i) sleep 0.1 end channel.close end loop do value = channel.receive break if value.nil? puts "Received: #{value}" end """ * **Why it Matters:** Without explicit thread-communication mechanisms like channels, unexpected behavior and data corruption can easily arise in multi-threaded programs. Crystal's design forces developers to be explicit about thread safety, increasing robustness. ## 4. Data Flow and Reactivity ### 4.1. Explicit Data Flow Ensure that data flows through the application in a clear and predictable manner. Avoid implicit data dependencies and hidden side effects. * **Do This:** Define explicit interfaces and contracts between components. **Example:** """crystal interface DataProvider def fetch_data : Array(String) end class APIDataProvider def fetch_data : Array(String) # Simulate fetching data from an API ["Data from API"] end end class DataProcessor def initialize(@data_provider : DataProvider) end def process_data : Array(String) data = @data_provider.fetch_data data.map { |item| item.upcase } end end # Usage api_provider = APIDataProvider.new processor = DataProcessor.new(api_provider) result = processor.process_data puts result # Output: ["DATA FROM API"] """ * **Why it Matters:** Makes it easier to understand how data is transformed and passed through the application. ### 4.2. Observables and Reactive Programming Although Crystal does not have native support for reactive programming frameworks like RxJS, you can implement similar patterns using channels and custom event handlers. * **Do This:** Create channels or custom event handlers using blocks/procs to react to state changes. This promotes a decoupled way of updating states based on external events or time. **Example:** """crystal require "concurrent" class Observable(T) getter subscribers : Array(Proc(T)?) def initialize @subscribers = [] of Proc(T)? end def subscribe(&block : Proc(T)) @subscribers << block end def unsubscribe(block : Proc(T)) @subscribers.delete(block) end def notify(value : T) @subscribers.each do |subscriber| subscriber.call(value) if subscriber end end end # Usage observable = Observable(Int32).new subscriber1 = ->(value : Int32) { puts "Subscriber 1: #{value}" } subscriber2 = ->(value : Int32) { puts "Subscriber 2: #{value * 2}" } observable.subscribe(&subscriber1) observable.subscribe(&subscriber2) observable.notify(10) # Output: # Subscriber 1: 10 # Subscriber 2: 20 observable.unsubscribe(subscriber1) observable.notify(20) # Output: # Subscriber 2: 40 """ * **Why it Matters:** Enables building responsive and event-driven applications. Very helpful when multiple components depend on a single piece of changing data. ### 4.3. Using Callbacks and Events for Decoupling Leverage callbacks and events to decouple components and allow them to react to state changes without direct dependencies. * **Do This:** Define event handlers and use callbacks to notify listeners of state updates. * **Don't Do This:** Directly modifying the state of other components within event handlers. **Example:** """crystal class Button alias ClickHandler = Proc(Nil) getter click_handlers : Array(ClickHandler) = [] of ClickHandler def on_click(&block : ClickHandler) @click_handlers << block end def click @click_handlers.each { |handler| handler.call } end end class UIUpdater def update_display puts "Display updated!" end end # Usage updater = UIUpdater.new button = Button.new button.on_click do updater.update_display end button.click # Output: Display updated! """ * **Why it Matters:** Promotes modularity and reduces coupling between components, making the code more flexible and easier to maintain. ## 5. Common Anti-Patterns and Mistakes ### 5.1. God Objects Avoid creating "God Objects" that manage too much state and logic within a single class. Break down large classes into smaller, more manageable components. * **Do This:** Apply the Single Responsibility Principle (SRP) and ensure each class has a clear and focused purpose. * **Don't Do This:** Adding unrelated responsibilities to a single class. ### 5.2. Global Mutable State Avoid using global mutable state whenever possible. It can lead to unpredictable behavior and make it difficult to reason about the code. * **Do This:** Favor local state or immutable data structures. When global state is necessary, use proper synchronization mechanisms to prevent race conditions. * **Don't Do This:** Directly modifying global variables from multiple threads without protection. ### 5.3. Tight Coupling Avoid tight coupling between components, where changes in one component require changes in other components. * **Do This:** Use dependency injection, interfaces, and events to decouple components. * **Don't Do This:** Directly accessing and modifying the internal state of other components. ### 5.4. Ignoring Thread Safety In concurrent applications, ignoring thread safety can lead to data corruption and crashes. * **Do This:** Always consider thread safety when working with shared state and use appropriate synchronization mechanisms (channels, locks, etc.). Understand Crystal's concurrency model. * **Don't Do This:** Assuming that code is automatically thread-safe without proper consideration. ## 6. Performance Considerations ### 6.1. Minimize State Updates Reduce the frequency of state updates to improve performance. Batch updates together or use techniques like debouncing or throttling to limit the number of updates. ### 6.2. Efficient Data Structures Use appropriate data structures for managing state. For example, use "Set" for storing unique values, "Hash" for fast lookups, and "StaticArray" for fixed-size arrays. Crystal's type inference often allows the compiler to optimize these for maximum speed. ### 6.3. Avoid Unnecessary Cloning Cloning large data structures can be expensive. Avoid cloning unless absolutely necessary. If you need to modify a copy of an object, consider using techniques like copy-on-write to defer the actual copying until necessary. ### 6.4 Pre-Allocation when possible When dealing with collections, try to pre-allocate memory to avoid re-allocations during runtime. The use of capacity hints can drastically improve performance when large changes are made in a loop. **Example:** """crystal array = Array(Int32).new(initial_capacity = 100) 100.times do |i| array << i end """ ## 7. Security Considerations ### 7.1. Validate Input Data Always validate input data before using it to update state. This can prevent vulnerabilities such as injection attacks and data corruption. ### 7.2. Sanitize Output Data Sanitize output data before displaying it to the user. This can prevent cross-site scripting (XSS) attacks. ### 7.3. Secure Storage of Sensitive Data Store sensitive data (e.g., passwords, API keys) securely. Use encryption, hashing, and secure storage mechanisms to protect sensitive information. Never store secrets in plain text in the code or configuration files. Use environment variables or dedicated secret management solutions. ### 7.4. Principle of Least Privilege Components should only have access to the state they need to perform their functions. Limit the scope of access to prevent unintended side effects and security vulnerabilities. ## 8. Conclusion Effective state management is crucial for building robust, maintainable, and scalable Crystal applications. By following these coding standards, developers can ensure that state is managed correctly, data flows predictably, and the application remains secure and performant. Embracing immutability, leveraging dependency injection, and understanding Crystal's concurrency model are key practices for successful Crystal development.
# Performance Optimization Standards for Crystal This document outlines performance optimization standards for Crystal applications. These standards aim to improve speed, responsiveness, and resource utilization. The guidelines are tailored to Crystal's features and ecosystem, focusing on modern best practices and avoiding common pitfalls. ## 1. Architectural Considerations ### 1.1 Choosing the Right Data Structures and Algorithms * **Do This:** Select data structures and algorithms appropriate for the task. Consider tradeoffs between memory usage and processing speed. Use profilers to identify performance bottlenecks and guide optimization efforts. * **Don't Do This:** Blindly use familiar data structures without considering their performance characteristics in the specific context. Avoid premature optimization without profiling. **Why:** The choice of data structure and algorithm greatly impacts performance. Poor choices lead to inefficient code, regardless of other optimizations. **Example:** """crystal # Efficiently counting word frequencies using a Hash def word_frequencies(text : String) : Hash(String, Int32) frequencies = Hash(String, Int32).new text.split.each do |word| word = word.downcase # normalize frequencies[word] = (frequencies[word] || 0) + 1 end frequencies end # Less efficient approach using an Array, especially as the text grows def word_frequencies_array(text : String) : Array(Tuple(String, Int32)) frequencies = [] of Tuple(String, Int32) words = text.split words.each do |word| word = word.downcase existing_index = frequencies.index { |tupl| tupl[0] == word } if existing_index frequencies[existing_index] = {word, frequencies[existing_index][1] + 1} else frequencies << {word, 1} end end frequencies end """ The "word_frequencies" method using "Hash" is significantly faster due to the O(1) average time complexity for lookups, compared to the "word_frequencies_array" which relies on "Array" with O(n) complexity for "index". ### 1.2 Concurrency and Parallelism * **Do This:** Leverage Crystal's lightweight concurrency features (fibers and channels) to handle I/O-bound tasks and parallelism where appropriate. Use the "spawn" keyword for concurrent execution. Use channels for safe communication between fibers. * **Don't Do This:** Overuse concurrency, as excessive context switching can degrade performance. Avoid sharing mutable state without proper synchronization mechanisms. **Why:** Concurrency improves responsiveness and throughput. Parallelism can reduce processing time on multi-core systems. **Example:** """crystal require "socket" def handle_client(client : TCPSocket) begin puts "Connection from #{client.remote_address}" loop do line = client.gets break if line.nil? response = "Echo: #{line}" # simulate some processing client.puts response end ensure client.close puts "Connection closed" end end server = TCPServer.new("0.0.0.0", 8080) puts "Server listening on port 8080" loop do client = server.accept spawn handle_client(client) # Handle each client in a separate fiber. end """ This example utilizes fibers to handle multiple client connections concurrently, preventing blocking behavior and improving server throughput. ### 1.3 Memory Management * **Do This:** Minimize memory allocations and deallocations, especially in performance-critical sections. Reuse objects where possible. Be mindful of large object allocations, which can cause pauses due to garbage collection. Use the object pool pattern for frequently used objects. Consider using "uninitialized: true" when allocating arrays where the contents will be immediately overwritten, to avoid unnecessary initialization. * **Don't Do This:** Create unnecessary copies of data. Ignore potential memory leaks (though Crystal's garbage collector mitigates many common leaks, excessive allocation still impacts performance). **Why:** Frequent memory operations can lead to significant overhead. Reducing allocations and reusing objects reduces the burden on the garbage collector. **Example:** """crystal # Object Pool Pattern class ReusableObject @@pool : Channel(ReusableObject) = Channel(ReusableObject).new def self.get : ReusableObject @@pool.receive || new end def release @@pool.send(self) end # instance variables for the object @data : String def initialize @data = "" end def process(input : String) @data = input.upcase puts @data end def clear @data = "" end end # Usage obj = ReusableObject.get obj.process("hello") obj.clear # Reset the object's state instead of discarding it obj.release # Return the object to the pool """ This pattern reduces the overhead of creating and destroying objects, particularly useful for frequently used objects in server contexts. ### 1.4 Use "GC.disable" with Caution * **Do This:** Consider using "GC.disable" temporarily if you're performing a large batch operation where new object allocation is minimal after the initial setup. **Ensure** you re-enable the GC ("GC.enable") as soon as the operation is complete. * **Don't Do This:** Use "GC.disable" globally or for extended periods, as this can lead to uncontrolled memory growth and eventual crashes if the GC is never re-enabled. **Why:** Disabling GC can reduce overhead during intensive operations, but it demands careful management. **Example:** """crystal GC.disable begin # Perform intensive operation with minimal new object allocation big_array = Array.new(1_000_000) { |i| i } # Process the array without creating many new objects big_array.each do |i| # Perform calculations here, minimizing allocations end ensure GC.enable # Ensure GC is re-enabled, even if an error occurs end """ ### 1.5 Profiling * **Do This:** Use profiling tools ("--profile", FlameScope, etc.) to identify performance bottlenecks. Analyze CPU usage, memory allocation, and time spent in different parts of your code. * **Don't Do This:** Rely on intuition or guesswork to optimize code without profiling data. **Why:** Profiling provides data-driven insights into performance issues, guiding optimization efforts effectively. **Example:** Run your Crystal program with profiling enabled: """bash crystal build your_program.cr --release --profile ./your_program """ Then use a tool like FlameScope to analyze the generated "profile.json" for CPU hotspots. ## 2. Code-Level Optimizations ### 2.1 Type Specialization * **Do This:** Take advantage of Crystal's static typing to allow the compiler to generate optimized machine code. Declare specific type constraints when possible. Use structs instead of classes when inheritance and polymorphism are not needed, as structs are stack-allocated and faster. Favor concrete types over abstract types where possible. * **Don't Do This:** Neglect type annotations, as this can hinder the compiler's ability to optimize. Overuse generic types if concrete types are known. **Why:** Static typing enables compile-time optimizations, reducing runtime overhead. **Example:** """crystal # Optimized with type specialization def add(x : Int32, y : Int32) : Int32 x + y end # Less optimized due to lack of type specificity (relies on Union type) def add_untyped(x, y) x + y # Compiler must perform runtime type checks if types are not known end """ The first "add" function allows the compiler to generate optimized machine code because types are known at compile time. The second function requires runtime type checks, which impacts performance. For best performance, inlined methods are best. ### 2.2 String Manipulation * **Do This:** Use "String#build" for efficient string concatenation, especially when dealing with a large number of concatenations. Prefer "String#[]=", "String#insert", and "String#<<" for in-place modifications when appropriate. Avoid unnecessary string allocations. Use "Slice" liberally for accessing substrings without copying. * **Don't Do This:** Use "+" or "String#concat" for repeated string concatenation within loops, as these create new string objects in each iteration. Create unnecessary copies of strings. **Why:** String operations are common in many applications, and inefficient string handling can be a source of performance bottlenecks. **Example:** """crystal # Efficient string concatenation def build_string(items : Array(String)) : String String.build do |io| items.each do |item| io << item end end end # Inefficient string concatenation def concat_string(items : Array(String)) : String result = "" items.each do |item| result += item # Creates a new string object in each iteration end result end """ The "build_string" function is much more efficient, especially for large arrays, because it avoids creating intermediate string objects. ### 2.3 Iteration and Looping * **Do This:** Use optimized iterators like "each", "each_with_index" when appropriate. When performance is critical and the number of iterations is known, use "(0...n).to_a.each" or iterative loops ("for i in 0...n") . Prefer "while" loops for simple iterative tasks. * **Don't Do This:** Use inefficient or unnecessarily complex looping constructs. Use "Array#size" repeatedly within a loop condition; cache the size instead. **Why:** Efficient iteration is crucial for processing collections of data. **Example:** """crystal # Efficient iteration using each array = [1, 2, 3, 4, 5] array.each do |element| puts element end # Inefficient: Repeatedly calling array.size within the loop condition array2 = [1, 2, 3, 4, 5] i = 0 while i < array2.size puts array2[i] i += 1 end # more efficient caching the size array3 = [1, 2, 3, 4, 5] size = array3.size i = 0 while i < size puts array3[i] i += 1 end """ ### 2.4 Inlining * **Do This:** Mark frequently called, small methods as "inline" to encourage the compiler to inline them. Profile your code to identify candidate methods for inlining. * **Don't Do This:** Inline large or infrequently called methods, as this can increase code size without providing a significant performance benefit. **Why:** Inlining reduces function call overhead, improving performance. **Example:** """crystal # Inlined method for fast access inline def square(x : Int32) : Int32 x * x end result = square(5) # The compiler may replace this call with the actual code of square """ ### 2.5 Allocation-Free Operations * **Do This:** Explore allocation-free APIs where available, especially in tight loops. For example use "IO::Memory" instead of creating strings. * **Don't Do This:** Unnecessarily allocate memory which can lead to GC pressure. **Why:** Reducing memory allocation is an important optimization since it reduces garbage collection overhead. **Example:** """crystal require "io" data = "some data" IO::Memory.new(data) do |io| # The underlying memory of "data" is directly read from, without any memory allocation. io.each_line do |line| puts line end end """ ### 2.6 Optimize Regular Expressions * **Do This:** Compile regular expressions ahead of time if they are used repeatedly. Use appropriate regex flags for performance. Consider simpler string operations if possible. * **Don't Do This:** Create and compile regular expressions repeatedly inside loops. Use overly complex regular expressions when simpler patterns suffice. **Why:** Regular expression compilation can be expensive. Optimized regex patterns improve matching speed. **Example:** """crystal # Compile regex once and reuse @@regex = /pattern/.freeze def process_data(data : String) if @@regex.match(data) puts "Match found" end end """ ### 2.7 Avoid Excessive Metaprogramming * **Do This:** Use metaprogramming judiciously to avoid runtime performance penalties if possible. * **Don't Do This:** Overuse metaprogramming when simpler, more direct approaches are available. **Why:** Metaprogramming can introduce runtime overhead due to dynamic code generation and evaluation. Compile-time metaprogramming with macros is generally much more performant. ### 2.8 Struct vs Class * **Do This:** Prefer "struct" when you have a value type that does not require inheritance, shared mutable state, or object identity. * **Don't Do This:** Use "class" when a "struct" would suffice. * **Why:** Structs are allocated on the stack - leading to faster allocation and deallocation and smaller memory footprint. Classes introduces GC overhead. **Example:** """crystal # Prefer Struct struct Point x : Int32 y : Int32 end # Avoid Class if structs work class PointClass x : Int32 y : Int32 end """ ### 2.9 C Bindings for Compute Intensive Tasks * **Do This:** If bottlenecks remain, consider using C bindings if Crystal implementation is slower than an existing C library. * **Don't Do This:** Use C bindings for tasks that Crystal can handle efficiently by itself. Prefer optimized Crystal code over unoptimized C code * **Why:** This allows you to leverage well-optimized C libraries for specific algorithms or functionalities. **Example:** """crystal lib SomeCLibrary fun c_function(arg : Int32) : Int32 end def crystal_function(arg : Int32) : Int32 SomeCLibrary.c_function(arg) end """ Be aware of the overhead incurred when crossing the C/Crystal boundary. ## 3. IO Optimization ### 3.1 Buffering * **Do This:** Use buffered IO operations when dealing with files and network connections. * **Don't Do This:** Perform unbuffered IO for large data streams, resulting in excessive system calls. **Why:** Buffering reduces the number of system calls. **Example:** """crystal File.open("large_file.txt", "r") do |file| buffered_reader = IO::Buffered::Reader.new(file) while line = buffered_reader.gets # Process line end end """ ### 3.2 Zero-Copy Techniques * **Do This:** Explore zero-copy techniques where possible, for tasks like sending files over a network socket. * **Don't Do This:** Create unnecessary copies of data when transferring data between IO streams. **Why:** Copying large chunks of data can add significant overhead. """crystal require "socket" def send_file(socket : TCPSocket, file_path : String) File.open(file_path, "r") do |file| socket.send_file(file) # Zero-copy send (if supported by the OS/socket) end end """ ## 4. Database Interactions ### 4.1 Connection Pooling * **Do This:** Use connection pooling to reuse database connections and reduce connection overhead. Consider libraries like "connection_pool" if appropriate. * **Don't Do This:** Create a new database connection for each request. **Why:** Establishing new database connections is an expensive operation. ### 4.2 Prepared Statements * **Do This:** Use prepared statements to prevent SQL injection and improve query performance. * **Don't Do This:** Concatenate user input directly into SQL queries. **Why:** Prepared statements are precompiled and can be executed multiple times with different parameters, avoiding recompilation overhead. ### 4.3 Data Serialization * **Do This:** Use efficient data serialization formats like Protocol Buffers, Avro, or MessagePack for inter-service communication or data storage. * **Don't Do This:** Use inefficient formats like JSON where binary formats are more appropriate. **Why:** Binary serialization formats are typically faster and produce smaller payloads. ## 5. Compiler Flags and Build Options ### 5.1 Release Mode Compilation * **Do This:** Always compile your code in release mode ("crystal build --release ...") for production deployments. * **Don't Do This:** Deploy debug builds to production, as they contain extra debugging information and lack optimizations. **Why:** Release mode enables optimizations, such as inlining and dead code elimination, which can significantly improve performance. ### 5.2 Target CPU Architecture * **Do This:** Specify the target CPU architecture ("crystal build --target=native ..." or "--target=x86_64-linux-gnu") to enable architecture-specific optimizations. * **Don't Do This:** Use generic build targets when deploying to specific hardware. **Why:** Architecture-specific optimizations can improve performance on the target hardware. ### 5.3 Link-Time Optimization (LTO) * **Do This:** Consider enabling LTO ("-Dlink_time_optimization") during compilation for potentially further optimizations involving interactions *between* compiled parts of your program. * **Don't Do This:** Use "LTO" during development because compilation will be slower. **Why:** LTO performs whole-program optimization at link time, which can result in better overall performance. ## 6. Caching ### 6.1 Result Caching * **Do This:** Cache expensive function results (e.g., database queries, complex calculations) to avoid redundant computations. Use memoization techniques for pure functions. * **Don't Do This:** Cache results indefinitely without considering cache invalidation. **Why:** Caching reduces the need to recompute results. **Example:** """crystal require "memoize" class DataFetcher include Memoize def fetch_data(query : String) : String # ... expensive database query ... sleep 1 # Simulate expensive action "Data for #{query}" end memoize :fetch_data end fetcher = DataFetcher.new puts fetcher.fetch_data("query1") # First call - takes time puts fetcher.fetch_data("query1") # Second call - returns cached result immediately """ ### 6.2 HTTP Caching * **Do This:** Implement HTTP caching (e.g., using "Cache-Control" headers) to reduce server load and improve client-side performance. Use a reverse proxy cache (e.g., Varnish, Nginx) to cache responses close to the client. * **Don't Do This:** Neglect HTTP caching, especially for static assets and frequently accessed API endpoints. **Why:** Browser caching and proxy caching reduce network traffic and server load. ## 7. Monitoring and Continuous Improvement ### 7.1 Performance Monitoring * **Do This:** Implement performance monitoring using tools like Prometheus, Grafana, or StatsD to track key metrics (e.g., response time, throughput, error rates). * **Don't Do This:** Operate in the dark without performance metrics. **Why:** Monitoring provides insights into application performance and helps identify areas for improvement. ### 7.2 Continuous Profiling * **Do This:** Regularly profile your application in production to identify performance regressions and new bottlenecks. * **Don't Do This:** Profile only during development. Production profiles offer a more realistic view of performance characteristics. **Why:** Production traffic patterns and data volumes can reveal performance issues that are not apparent in development environments. By adhering to these performance optimization standards, Crystal developers can build high-performance applications that are efficient, responsive, and scalable. Remember to prioritize profiling, testing, and continuous monitoring to ensure ongoing performance improvements.