## Overview
This document outlines the standards and best practices for developing an Elixir library. These guidelines ensure the library remains reliable, maintainable, secure, and easy to integrate into other Elixir applications.
This is a pure Elixir library intended to be used as a dependency in other Elixir applications. It is **not** a Phoenix or Nerves project. Instead, it focuses on providing functional building blocks using idiomatic Elixir and OTP constructs.
## Core Principles
- Write clean, composable, and testable code
- Adhere to functional programming principles—avoid OOP patterns
- Maintain clear boundaries between modules and domains
- Ensure code is robust, secure, and easy to reason about
- Provide rich documentation and helpful logging
- Create libraries that integrate seamlessly into any Elixir application
## Project Structure
### Directory Layout
```
.
├── lib/
│ ├── your_library/
│ │ ├── core/ # Core functionality and behaviors
│ │ ├── components/ # Main component modules
│ │ ├── otp/ # OTP components (supervisors, workers)
│ │ ├── utils/ # Utility functions and helpers
│ │ └── types/ # Custom types and specs
│ └── your_library.ex # Main entry point
├── test/
│ ├── your_library/
│ │ ├── core/ # Tests mirroring lib structure
│ │ ├── components/
│ │ └── otp/
│ ├── support/ # Test helpers and shared fixtures
│ └── test_helper.exs
├── mix.exs
└── mix.lock
```
### Structural Guidelines
- **Data First**: Define data structures and types before implementing operations on them
- **Module Organization**: Group modules by domain or functionality
- **Test Mirroring**: Tests should mirror the directory structure
- **Minimal Dependencies**: Avoid circular dependencies between modules
- **Clear Boundaries**: Each module should have a single responsibility
## Code Organization
### Data Structure Definition
1. Start with pure data structures using structs:
```elixir
defmodule YourLibrary.Types.Task do
use TypedStruct
typedstruct do
field :id, String.t()
field :name, String.t()
field :status, :pending | :running | :completed
field :created_at, DateTime.t()
end
@type validation_error ::
:invalid_name |
:invalid_status |
{:invalid_date, String.t()}
@spec validate(t()) :: :ok | {:error, validation_error()}
def validate(%__MODULE__{} = task) do
# Validation logic
end
end
```
2. Then define modules that operate on these structures:
```elixir
defmodule YourLibrary.Core.TaskOperations do
alias YourLibrary.Types.Task
@spec create_task(String.t()) :: {:ok, Task.t()} | {:error, Task.validation_error()}
def create_task(name) do
task = %Task{
id: generate_id(),
name: name,
status: :pending,
created_at: DateTime.utc_now()
}
case Task.validate(task) do
:ok -> {:ok, task}
{:error, _reason} = error -> error
end
end
end
```
3. Finally, implement process lifecycle modules:
```elixir
defmodule YourLibrary.OTP.TaskManager do
use GenServer
alias YourLibrary.Core.TaskOperations
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(opts) do
{:ok, %{tasks: %{}, opts: opts}}
end
# ... rest of GenServer implementation
end
```
### Function Heads and Guards
Use multiple function heads for clarity and control flow:
```elixir
defmodule YourLibrary.Core.DataProcessor do
# Match on specific values
def process(:empty), do: {:ok, []}
# Use guards for type checking
def process(data) when is_list(data) do
{:ok, Enum.map(data, &transform/1)}
end
# Pattern match on complex structures
def process(%{items: items, status: :ready} = data)
when is_list(items) and length(items) > 0 do
{:ok, process_items(items, data)}
end
# Catch-all case
def process(_invalid) do
{:error, :invalid_input}
end
# Private functions can also use guards
defp transform(item) when is_binary(item) do
String.upcase(item)
end
defp transform(item) when is_integer(item) do
Integer.to_string(item)
end
end
```
### Behaviors
Define behaviors to establish contracts between modules:
```elixir
defmodule YourLibrary.Core.Processor do
@doc """
Defines the contract for processing data.
"""
@callback process(data :: term()) ::
{:ok, term()} |
{:error, term()}
@doc """
Optional callback for data validation.
"""
@callback validate(input :: term()) ::
:ok |
{:error, term()}
@optional_callbacks validate: 1
# Can include default implementations
defmacro __using__(_opts) do
quote do
@behaviour YourLibrary.Core.Processor
# Default implementation for validate
@impl true
def validate(_input), do: :ok
# Allow overrides
defoverridable validate: 1
end
end
end
# Implementation example
defmodule YourLibrary.Core.StringProcessor do
use YourLibrary.Core.Processor
@impl true
def process(data) when is_binary(data) do
{:ok, String.upcase(data)}
end
@impl true
def validate(input) when is_binary(input) do
if String.valid?(input), do: :ok, else: {:error, :invalid_string}
end
end
```
## Code Quality Standards
### Formatting and Style
- Run `mix format` before committing code
- Use [Credo](https://hex.pm/packages/credo) for static analysis
- Follow standard Elixir style guide
### Documentation Requirements
- Add `@moduledoc` to every module
- Add `@doc` to every public function
- Include examples in documentation using `@example` when helpful
- Document not just what functions do, but why and how
- Generate documentation with [ExDoc](https://hex.pm/packages/ex_doc)
### Type Specifications
```elixir
@type my_type :: String.t() | atom()
@spec my_function(my_type) :: {:ok, term()} | {:error, term()}
def my_function(input) do
# Implementation
end
```
- Use `@type` and `@typep` for type definitions
- Add `@spec` for all public functions
- Keep type specs accurate and descriptive
- Use Dialyzer for static type checking
### Naming Conventions
- Use `snake_case` for functions and variables
- Use `PascalCase` for module names
- Choose descriptive names over terse ones
- Follow Elixir community conventions
## Functional Programming Guidelines
### Pure Functions
```elixir
# Prefer
def process_data(data) do
{:ok, transform(data)}
end
# Over
def process_data(data) do
save_to_disk(transform(data))
end
```
- Keep functions pure when possible
- Return tagged tuples (`{:ok, value}` or `{:error, reason}`)
- Avoid side effects in core logic
- Use pattern matching over conditional logic
### OTP Integration
```elixir
defmodule YourLibrary.Application do
use Application
def start(_type, _args) do
children = [
{YourLibrary.Server, []},
{YourLibrary.Cache, []}
]
opts = [strategy: :one_for_one, name: YourLibrary.Supervisor]
Supervisor.start_link(children, opts)
end
end
```
- Structure OTP components for easy integration
- Use supervision trees appropriately
- Implement proper shutdown handling
- Follow OTP conventions and patterns
## Error Handling
### Error Pattern
```elixir
def complex_operation(input) do
with {:ok, data} <- validate(input),
{:ok, processed} <- process(data),
{:ok, result} <- format(processed) do
{:ok, result}
else
{:error, reason} -> {:error, reason}
end
end
```
- Use `with` statements for complex operations
- Return tagged tuples consistently
- Create custom error types when needed
- Avoid silent failures
### Logging
```elixir
require Logger
def important_function(arg) do
Logger.info("Processing #{inspect(arg)}")
# Implementation
rescue
e ->
Logger.error("Failed to process: #{inspect(e)}")
{:error, :processing_failed}
end
```
- Use appropriate log levels
- Include context in log messages
- Avoid logging sensitive data
- Configure logger in consuming applications
- Use Logger.info/error/warning/debug/error/critical
## Testing Standards
### Test Organization
```elixir
defmodule YourLibraryTest.Core.ProcessorTest do
use ExUnit.Case, async: true
alias YourLibrary.Core.StringProcessor
describe "process/1" do
test "processes valid string data" do
assert {:ok, "HELLO"} = StringProcessor.process("hello")
end
test "returns error for invalid input" do
assert {:error, _} = StringProcessor.process(123)
end
end
describe "validate/1" do
test "validates string input" do
assert :ok = StringProcessor.validate("valid")
assert {:error, :invalid_string} = StringProcessor.validate(<<255>>)
end
end
end
```
- Append `Test` to the module name
- Write comprehensive unit tests
- Use property-based testing where appropriate
- Maintain test readability
- Ensure tests are deterministic
### Test Coverage
- Aim for high test coverage
- Test edge cases and error conditions
- Include doctests for examples
- Use ExUnit tags for test organization
## Configuration
### Server Configuration
```elixir
# In config/config.exs of consuming application
config :your_library,
key: "value",
timeout: 5000
```
- Use application configuration
- Allow server configuration
- Provide sensible defaults
- Document all configuration options
## Versioning and Release
- Follow semantic versioning
- Maintain a CHANGELOG.md
- Tag releases in version control
- Update documentation with releases
## Security Considerations
- Handle sensitive data appropriately
- Validate all inputs
- Document security considerations
- Follow security best practices
## Performance
- Optimize only with benchmarks
- Document performance characteristics
- Consider resource usage
- Implement timeouts where appropriate
less
rest-api
elixir
First Time Repository
All Repositories (2)
2067
A foundational framework for building autonomous, distributed agent systems in Elixir.