ExEssentials
ExEssentials is a powerful utility library for Elixir that serves as a true toolbox — bringing together a collection of generic, reusable, and ready-to-use helpers to accelerate Elixir application development.
Designed with a focus on productivity and organization, it helps you write cleaner, more maintainable code while saving valuable development time.
Features
- 🛠 Flow Builder (Runner): Model complex business logic as a sequence of named steps (sync or async) with built-in error recovery.
- 🔑 Fingerprinting: Generate deterministic SHA-256 hashes and compare maps or structs by selected keys.
- 🇧🇷 Brazilian Document Validation: Robust CPF and CNPJ (numeric & alphanumeric) validation and formatting.
- 🌐 Web & API Helpers: RFC 7807 compliant request validation, query parameter normalization, and service toggling.
- 📑 XML Utilities: Safe and sanitized XML building built on top of Saxy.
- 🗺 Map Utilities: Clean way to rename keys, compact
nil, or blank values from maps.
Installation
The package can be installed by adding ex_essentials to your list of dependencies in mix.exs:
def deps do
[
{:ex_essentials, "~> 0.11.0"}
]
end
Usage
🚀 Core Runner (Flow Builder)
Inspired by Ecto.Multi, the Runner allows you to build fail-fast execution flows with named steps.
alias ExEssentials.Core.Runner
runner =
Runner.new(timeout: 5_000)
|> Runner.put(:user_id, 123)
|> Runner.run(:fetch_user, fn %{user_id: id} -> {:ok, %{id: id, name: "John"}} end)
|> Runner.run_async(:send_email, fn _ -> {:ok, :sent} end)
|> Runner.run(:log_action, fn %{fetch_user: user} -> {:ok, "Logged #{user.name}"} end)
case Runner.finish(runner) do
{:ok, changes} ->
IO.puts("User fetched: #{changes.fetch_user.name}")
{:error, step, reason, changes_before} ->
IO.inspect({step, reason}, label: "Flow failed")
end
🔑 Fingerprint Utilities
Generate SHA-256 fingerprint hashes and compare maps or structs based on a subset of keys.
alias ExEssentials.Core.Fingerprint
# Generate hash for selected fields
user = %{id: 1, name: "Alice", email: "alice@example.com"}
Fingerprint.hash(user, [:id, :email])
# => "..."
# Compare two maps or structs by selected keys
user_a = %{id: 1, name: "Alice", role: :admin}
user_b = %{id: 1, name: "Alice", role: :user}
Fingerprint.equal?(user_a, user_b, [:id, :name]) # => true
Fingerprint.equal?(user_a, user_b, [:id, :role]) # => false
🇧🇷 Brazilian Document Validation
Validate, format, and mask CPF and CNPJ numbers effortlessly. Supports alphanumeric CNPJs and automatic digit extraction.
Validation & Formatting
alias ExEssentials.BrazilianDocument.Validator
alias ExEssentials.BrazilianDocument.Formatter
# Validate CPF or CNPJ (Numeric or Alphanumeric)
Validator.valid?("123.456.789-00") # => true
Validator.valid?("12ABC34501DE35") # => true (New Alphanumeric CNPJ)
# Format for display
Formatter.format("39053344705") # => "390.533.447-05"
Formatter.format("11222333000181") # => "11.222.333/0001-81"
# Mask sensitive data
Formatter.mask("44286185060") # => "***.861.850-**"
Formatter.mask("20495056000171") # => "20.***.***/0001-7*"
Ecto Integration
Easily validate documents in your changesets.
defmodule MyApp.User do
use Ecto.Schema
import Ecto.Changeset
import ExEssentials.BrazilianDocument.Changeset
schema "users" do
field :document, :string
end
def changeset(user, attrs) do
user
|> cast(attrs, [:document])
|> validate_brazilian_document(:document) # Validates CPF or CNPJ
# Or specifically:
# |> validate_cpf(:document)
# |> validate_cnpj(:document)
end
end
🛡 Web Utilities (Plugs & Validators)
Request Validation
A Plug for validating request parameters using Ecto.Changeset. If validation fails, it returns a 400 Bad Request following RFC 7807.
# 1. Define your validator (mapping actions to changesets)
defmodule MyAppWeb.Validators.User do
import Ecto.Changeset
def create(params) do
{%{}, %{name: :string, email: :string}}
|> cast(params, [:name, :email])
|> validate_required([:name, :email])
end
end
# 2. Use the plug in your controller
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
plug ExEssentials.Web.Plugs.RequestValidator, validator: MyAppWeb.Validators.User
def create(conn, params) do
# Only executes if params are valid
text(conn, "User Created")
end
end
Query Parameter Normalization
Automatically converts query string values (like "true", "null", "123", or lists "1,2,3") into their Elixir types.
# In your endpoint.ex or router pipeline
plug ExEssentials.Web.Plugs.NormalizeQueryParams
# Input: /api/search?active=true&tags=elixir,rust&limit=10
# Output: %{"active" => true, "tags" => ["elixir", "rust"], "limit" => 10}
Feature Flag Route Disabling
Disable specific controller actions based on feature flags (supports FunWithFlags).
# In your controller
plug ExEssentials.Web.Plugs.DisableServices,
disabled_actions: [:delete, :update],
flag_name: :maintenance_mode
📑 XML Utilities
A safe wrapper around Saxy to build XML documents with automatic sanitization.
import ExEssentials.XML
# Automatically escapes <, >, &, etc.
element_sanitize("note", [], ["Some <script> & unsafe content"])
# => {"note", [], ["Some <script> & unsafe content"]}
🗺 Map Utilities
Helpers for transforming and cleaning up map structures.
alias ExEssentials.Core.Map, as: MapUtil
# Renaming keys
map = %{name: "Alice", age: 30}
MapUtil.renake(map, [:name, age: :years])
# => %{name: "Alice", years: 30}
# Cleaning up maps (removing nil or blank values)
MapUtil.compact(%{a: 1, b: nil, c: "", d: [], e: %{}})
# => %{a: 1}
Configuration
You can customize the RequestValidator response format:
config :ex_essentials, :web_request_validator,
json_library: Jason,
error_code: :invalid_parameter,
error_title: "Invalid request parameters"
Contributing
- Fork it
- Create your feature branch (
git checkout -b feature/my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin feature/my-new-feature) - Create a new Pull Request
License
ExEssentials is released under the Apache License 2.0. See the LICENSE.txt file for details.