# 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.
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.
# 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.
# Testing Methodologies Standards for Crystal This document outlines the testing methodologies standards for Crystal projects. Following these guidelines will help ensure the creation of reliable, maintainable, and performant Crystal applications. ## 1. General Testing Principles * **Do This:** Embrace a test-driven development (TDD) or behavior-driven development (BDD) approach whenever possible. Write tests *before* implementing the corresponding functionality. * **Why:** TDD/BDD forces you to think about the functionality's purpose and interface before implementation, leading to better design and more comprehensive test coverage. * **Don't Do This:** Write tests as an afterthought. Neglecting testing leads to fragile codebases, increased debugging time, and higher maintenance costs. * **Do This:** Aim for high test coverage (ideally above 80%). Use tools like "crystal spec" with coverage reporting to measure coverage. * **Why:** Higher coverage significantly reduces the risk of regressions and undetected bugs. Be mindful that coverage is only part of the equation; test quality also matters. * **Don't Do This:** Equate high coverage with perfect testing. High coverage without meaningful assertions is useless. Focus on testing critical paths and edge cases. * **Do This:** Write focused and independent tests. * **Why:** Independent tests can be run in parallel, reducing test suite execution time. Focused tests make debugging failing tests easier. * **Don't Do This:** Create tests that depend on specific data or external state. Use mocking or test doubles to isolate units under test. * **Do This:** Maintain your test suite. Refactor tests as your codebase evolves. Delete or update outdated tests. * **Why:** An unmaintained test suite becomes a burden rather than an asset. Regularly pruning and updating tests ensures continued relevance and value. ## 2. Unit Testing ### 2.1. Purpose * Unit tests verify the behavior of individual components (classes, modules, or functions) in isolation. The goal is to validate that each unit performs its intended function correctly. ### 2.2. Standards * **Do This:** Use the built-in "spec" framework for unit testing. * **Why:** It's the standard testing library and integrates seamlessly with Crystal. """crystal require "spec" describe "Calculator" do it "adds two numbers correctly" do calculator = Calculator.new calculator.add(2, 3).should eq(5) end it "subtracts two numbers correctly" do calculator = Calculator.new calculator.subtract(5, 2).should eq(3) end end """ * **Do This:** Follow the Arrange-Act-Assert (AAA) pattern in your tests. * **Why:** AAA promotes clarity and readability by dividing the test into distinct phases. """crystal it "calculates area of a rectangle" do # Arrange rectangle = Rectangle.new(width: 5, height: 10) # Act area = rectangle.area # Assert area.should eq(50) end """ * **Do This:** Use mocks and stubs to isolate the unit under test. * **Why:** Mocks and stubs replace dependencies with controlled substitutes, preventing side effects and ensuring predictable test results. """crystal require "spec" require "mock" # Install: shards install mock class ExternalService def get_data # Imagine this performs an expensive network call raise "Not implemented" end end class MyClass def initialize(@service : ExternalService) end def process_data data = @service.get_data "Processed: #{data}" end end describe MyClass do it "processes data from external service" do mock_service = Mock.new(ExternalService) mock_service.should_receive(:get_data).and_return("mocked data") my_class = MyClass.new(mock_service.object) result = my_class.process_data result.should eq("Processed: mocked data") mock_service.verify end end """ * **Don't Do This:** Make your tests too tightly coupled to the implementation details. * **Why:** Tests that directly depend on internal implementation become brittle and break easily when the code is refactored. Favor testing public interfaces and observable behavior. * **Do This:** Test for exceptions or error conditions. * **Why:** Verifying that your code handles errors gracefully is crucial for reliability. """crystal it "raises an exception when dividing by zero" do calculator = Calculator.new expect_raises(ArgumentError) do calculator.divide(10, 0) end end """ * **Do This:** Use data providers or parameterized tests to cover various input values with a single test case. """crystal require "spec" describe "StringUtil" do { "hello world" => "HelloWorld", "foo bar baz" => "FooBarBaz", "a b c" => "ABC" }.each do |input, expected| it "capitalizes words in '#{input}' to '#{expected}'" do StringUtil.capitalize_words(input).should eq(expected) end end end """ ### 2.3 Common Anti-Patterns 1. **Testing private methods directly:** Generally, avoid this unless absolutely necessary. Focus on testing public interfaces. If a private method requires extensive testing, it might indicate that it should be extracted into its own class. 2. **Over-mocking:** Mocking every single dependency can lead to tests that are too specific and don't actually verify meaningful behavior. Use mocks strategically, focusing on external services or complex dependencies. Prefer integration tests for verifying interactions between internal components when appropriate. 3. **Ignoring edge cases:** Make sure to consider edge cases, boundary conditions, and invalid input when writing unit tests. These are often the source of bugs. ## 3. Integration Testing ### 3.1. Purpose * Integration tests verify the interaction between multiple components or systems. They ensure that units work together correctly. This might involve testing components within your application or testing interactions with external services (databases, APIs, etc.). ### 3.2. Standards * **Do This:** Use integration tests to verify that different parts of your application work together as expected. * **Why:** Unit tests isolate components, but integration tests ensure that these components interact correctly in a real-world scenario. """crystal require "spec" require "sqlite3" describe "User Registration" do it "creates a new user in the database" do # Arrange db = SQLite3::Database.new(":memory:") # Use an in-memory database for testing db.execute <<-SQL CREATE TABLE users ( id INTEGER PRIMARY KEY, username VARCHAR(255) ); SQL user_repo = UserRepository.new(db) username = "testuser" # Act user_repo.create(username) # Assert result = db.execute("SELECT username FROM users WHERE username = ?", username) result.should eq([[username]]) db.close end end class UserRepository def initialize(@db : SQLite3::Database) end def create(username : String) @db.execute("INSERT INTO users (username) VALUES (?)", username) end end """ * **Do This:** Use real dependencies whenever possible, but consider using test databases or mock APIs to avoid side effects and ensure reproducible tests. * **Why:** Using real dependencies provides a more realistic testing environment. * **Do This:** Properly isolate integration tests using transactions or other techniques to prevent interference between tests. * **Why:** Shared state between integration tests can lead to flaky and unpredictable results. """crystal # Example using transactions for database integration tests before_each do @db = SQLite3::Database.new(":memory:") @db.execute("BEGIN TRANSACTION") # Start a transaction @user_repo = UserRepository.new(@db) @db.execute <<-SQL CREATE TABLE users ( id INTEGER PRIMARY KEY, username VARCHAR(255) ); SQL end after_each do @db.execute("ROLLBACK TRANSACTION") # Rollback the transaction @db.close end """ Using this pattern ensures each tests starts with a clean database and avoids interfering with other tests. * **Don't Do This:** Run integration tests against production databases or live APIs. * **Why:** Integration tests should not modify or depend on production data. ### 3.3. Common Anti-Patterns 1. **Skipping integration tests:** Relying solely on unit tests can lead to missed integration issues, where individual components work fine but fail to interact correctly. 2. **Overlapping unit and integration tests:** Clearly define the scope of each type of test. Unit tests focus on individual units, while integration tests verify interactions between components. 3. **Ignoring database migrations:** Always test database migrations as part of your integration test suite to ensure that schema changes are applied correctly. ## 4. End-to-End (E2E) Testing ### 4.1. Purpose * E2E tests simulate real user interactions with the application, verifying that the entire system works correctly from start to finish. These tests typically involve a user interface (web browser, mobile app) and multiple backend services. ### 4.2. Standards * **Do This:** Use tools like [Selenium](https://www.selenium.dev/) or [Capybara](https://github.com/teamcapybara/capybara) (via appropriate Crystal bindings if available, or through system calls to external processes) for web application E2E testing. Consider libraries such as playwright-cr for more modern browser automation. * **Why:** These tools allow you to automate browser interactions and verify the behavior of your application from a user's perspective. """crystal # Illustration of a simple E2E test concept (requires external setup and browser driver) it "allows a user to register and log in" do # Simulate user actions: # 1. Open the registration page # 2. Fill in the registration form # 3. Submit the form # 4. Verify that the user is redirected to the login page # 5. Fill in the login form # 6. Submit the form # 7. Verify that the user is logged in and redirected to the dashboard # Assertions: # - Check that the user is logged in by verifying the presence of a welcome message # - Check that the user can access protected resources # - Check that the user can log out successfully end """ Note: The above is a conceptual example since a full selenium example requires significant setup including selenium server, appropriate drivers for your chosen web browser, and potentially system call bindings from crystal. * **Do This:** Write E2E tests that cover critical user flows, such as registration, login, checkout, etc. * **Why:** Focus on the most important user journeys to ensure that the core functionality of your application is working as expected. * **Do This:** Use a dedicated testing environment for E2E tests to avoid impacting production data. * **Why:** E2E tests often involve modifying data, so it's crucial to isolate them from production environments. * **Don't Do This:** Make your E2E tests too granular. Focus on testing entire user flows rather than individual UI elements. * **Why:** E2E tests are more expensive to run and maintain than unit or integration tests, so it's important to strike a balance between coverage and efficiency. ### 4.3. Common Anti-Patterns 1. **Ignoring E2E tests:** Skipping E2E tests can lead to critical bugs in the user interface or core workflows. 2. **Flaky E2E tests:** E2E tests are often prone to flakiness due to timing issues, network latency, or external dependencies. Implement retry mechanisms and improve test isolation to mitigate flakiness. 3. **Slow E2E tests:** Optimize your E2E test suite to reduce execution time. Run tests in parallel and use efficient selectors to locate UI elements. ## 5. Performance Testing ### 5.1. Purpose * Performance testing assesses the responsiveness, stability, and scalability of your application under various load conditions. It helps identify bottlenecks and optimize performance. ### 5.2. Standards * **Do This:** Use tools like [wrk](https://github.com/wg/wrk) or [hey](https://github.com/rakyll/hey) or [vegeta](https://github.com/tsenart/vegeta) to simulate load on your application. * **Why:** These tools allow you to measure the performance of your application under different load scenarios. """bash # Example using wrk to benchmark a web server wrk -t12 -c400 -d30s http://localhost:3000/ # -t: Number of threads # -c: Number of connections # -d: Duration of the test """ * **Do This:** Use profiling tools to identify performance bottlenecks in your code. Crystal's standard library includes "Profiler". * **Why:** Profilers help you pinpoint the areas of your code that are consuming the most resources. """crystal require "profiler" Profiler.start # Your code here Profiler.stop Profiler.print(STDOUT) """ The Profiler provides basic CPU profiling. More advanced tools may integrate with the operating system directly. Be sure to read the documentation for the tool you are using, and understand the output it produces. * **Do This:** Define performance metrics and set performance budgets for your application. * **Why:** Performance budgets help you track and maintain the performance of your application over time. Examples may include: request latency, throughput, error rate, CPU usage, memory usage. * **Do This:** Automate performance tests and integrate them into your CI/CD pipeline. * **Why:** Automated performance testing allows you to detect performance regressions early in the development cycle. * **Don't Do This:** Ignore performance testing until late in the development cycle. * **Why:** Performance issues can be difficult and costly to fix late in the development cycle. ### 5.3. Common Anti-Patterns 1. **Lack of performance testing:** Neglecting performance testing can lead to slow and unresponsive applications. 2. **Testing in unrealistic environments:** Performance tests should be conducted in an environment that closely resembles the production environment. 3. **Ignoring performance regressions:** Monitor performance metrics and address regressions promptly. ## 6. Security Testing ### 6.1. Purpose * Security testing identifies vulnerabilities in your application that could be exploited by attackers. It helps ensure the confidentiality, integrity, and availability of your data. ### 6.2. Standards * **Do This:** Perform static analysis of your code to identify potential security vulnerabilities. Consider using tools that look for common coding flaws, such as those that might lead to injection attacks. * **Why:** Static analysis can detect vulnerabilities early in the development cycle before they are introduced into production. * **Do This:** Perform dynamic analysis to identify vulnerabilities in your application at runtime. * **Why:** Dynamic analysis can detect vulnerabilities that are not detectable through static analysis. * **Do This:** Conduct penetration testing to simulate real-world attacks on your application. * **Why:** Penetration testing helps identify vulnerabilities that might be missed by automated tools. * **Do This:** Follow security best practices, such as input validation, output encoding, and least privilege. * **Why:** These practices help prevent common security vulnerabilities. * **Don't Do This:** Ignore security testing or assume that your application is secure by default. * **Why:** Security vulnerabilities can have serious consequences for your organization and your users. ### 6.3. Common Anti-Patterns 1. **Lack of security testing:** Neglecting security testing can leave your application vulnerable to attack. 2. **Relying solely on automated tools:** Automated tools can be helpful, but they cannot replace human expertise. 3. **Ignoring security updates:** Keep your dependencies up to date to patch known security vulnerabilities. ## 7. Test Doubles: Mocks, Stubs, and Fakes * **Understanding the Purpose:** Test doubles are essential tools for isolating units under test. ### 7.1. Mocks * **Do This:** Use mocks to verify that a dependent object is called with the correct arguments and in the correct order. Mocks are about behavior verification. * **Why:** Mocks allow you to assert that specific interactions occur between the unit under test and its dependencies """crystal require "spec" require "mock" class Notifier def send_email(recipient : String, message : String) puts "Sending email to #{recipient}: #{message}" # In real code, this would send an actual email end end class UserRegistration def initialize(@notifier : Notifier) end def register(email : String) # Registration logic here message = "Welcome to our service!" @notifier.send_email(email, message) end end describe UserRegistration do it "sends a welcome email after registration" do mock_notifier = Mock.new(Notifier) mock_notifier.should_receive(:send_email).with("test@example.com", "Welcome to our service!") registration = UserRegistration.new(mock_notifier.object) registration.register("test@example.com") mock_notifier.verify end end """ ### 7.2. Stubs * **Do This:** Use stubs to provide canned responses to method calls. Stubs are about state verification. * **Why:** Stubs allow you to control the return values of dependencies to simulate different scenarios. """crystal require "spec" class PaymentGateway def process_payment(amount : Float) : Bool raise "Not Implemented" end end class OrderService def initialize(@gateway : PaymentGateway) end def process_order(amount : Float) : Bool @gateway.process_payment(amount) end end describe OrderService do it "successfully processes an order when payment is successful" do # Arrange successful_gateway = Object.new.tap do |gateway| def gateway.process_payment(amount : Float) : Bool true # Stubbed to always return true end end order_service = OrderService.new(successful_gateway) # Act result = order_service.process_order(100.0) # Assert result.should be_true end it "fails to process order when payment fails" do # Arrange failed_gateway = Object.new.tap do |gateway| def gateway.process_payment(amount : Float) : Bool false # Stubbed to always return false end end order_service = OrderService.new(failed_gateway) # Act result = order_service.process_order(100.0) # Assert result.should be_false end end """ ### 7.3. Fakes * **Do This:** Use fakes to create simplified implementations of dependencies that mimic the behavior of the real ones but are easier to control in tests (e.g., in-memory database). * **Why:** Fakes provide a lightweight alternative to real dependencies, improving test performance and isolation. """crystal require "spec" class UserRepository def save(user : User) raise "Not Implemented" end def find(id : Int32) : User? raise "Not Implemented" end end class InMemoryUserRepository < UserRepository def initialize @users = {} of Int32 => User @next_id = 1 end def save(user : User) if user.id.nil? user.id = @next_id @next_id += 1 end @users[user.id] = user end def find(id : Int32) : User? @users[id] end end struct User property id : Int32? property name : String end describe InMemoryUserRepository do it "saves and retrieves a user" do repo = InMemoryUserRepository.new user = User.new(id: nil, name: "John Doe") repo.save(user) retrieved_user = repo.find(user.id) retrieved_user.should eq(user) end end """ ## 8. Version Specific Considerations * **Crystal 1.0+:** Ensure compatibility with the latest stable version of Crystal. Use features introduced in recent versions to write more concise and efficient tests. Pay attention to deprecation warnings and update your tests accordingly. Refer to the Crystal release notes for specifics. * **Shards:** Keep your testing dependencies (like "mock.cr") up to date by using "shards update". * **Crystal Tooling:** Utilize "crystal tool format" and Ameba as part of your pre-commit hooks or CI process to ensure consistent code formatting and identify potential issues. By adhering to these testing methodologies standards, Crystal developers can build robust, maintainable, and high-performing applications. Remember to adapt these guidelines to the specific needs and context of your projects.