towerops/lib/towerops/result.ex
Graham McIntire efaf5558ff refactor: convert 6 Gleam modules to idiomatic Elixir with TDD (#196)
Phase 1: Foundation Types (100% complete)
- query_helpers: SQL LIKE sanitization with pipe operators
- numeric: Integer parsing with pattern matching guards
- result: Pure Elixir Result monad (map, and_then, unwrap_or)

Phase 2: Ecto Domain Types (100% complete)
- ip_address: IPv4/IPv6 validation using :inet directly
- mac_address: Multi-format MAC parsing (colon/hyphen/dot/compact)
- snmp_oid: OID parsing/manipulation with recursive pattern matching

All 198 tests passing across converted modules.
API changed from Gleam-style {:some/:none to idiomatic {:ok/:error.
Refactored parse_numeric_oid to use with statement, reducing nesting depth.

Reviewed-on: graham/towerops-web#196
2026-03-28 09:52:07 -05:00

129 lines
3.5 KiB
Elixir

defmodule Towerops.Result do
@moduledoc """
Generic result type for representing success or failure outcomes.
This module provides a type-safe way to handle operations that can succeed or fail,
using Elixir's standard {:ok, value} | {:error, reason} pattern with enhanced type safety.
## Type Parameters
- `ok_value`: The type of value returned on success
- `error_value`: The type of error returned on failure
## Examples
# Basic usage with string success and atom error
@spec fetch_user(id :: String.t()) :: Result.t(User.t(), :not_found | :timeout)
def fetch_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
# Using helper functions
Result.map({:ok, user}, &User.display_name/1)
# => {:ok, "John Doe"}
Result.map_error({:error, :db_error}, fn _ -> :service_unavailable end)
# => {:error, :service_unavailable}
"""
@type t(ok_value, error_value) :: {:ok, ok_value} | {:error, error_value}
@type t(ok_value) :: t(ok_value, term())
@doc """
Maps the success value using the given function.
Error values are passed through unchanged.
## Examples
iex> Result.map({:ok, 5}, fn x -> x * 2 end)
{:ok, 10}
iex> Result.map({:error, :not_found}, fn x -> x * 2 end)
{:error, :not_found}
"""
@spec map(t(a, e), (a -> b)) :: t(b, e) when a: var, b: var, e: var
def map({:ok, value}, fun), do: {:ok, fun.(value)}
def map({:error, _} = error, _fun), do: error
@doc """
Maps the error value using the given function.
Success values are passed through unchanged.
## Examples
iex> Result.map_error({:error, :timeout}, fn _ -> :network_error end)
{:error, :network_error}
iex> Result.map_error({:ok, 42}, fn _ -> :network_error end)
{:ok, 42}
"""
@spec map_error(t(a, e), (e -> f)) :: t(a, f) when a: var, e: var, f: var
def map_error({:ok, _} = success, _fun), do: success
def map_error({:error, error}, fun), do: {:error, fun.(error)}
@doc """
Chains together operations that return results.
If the first result is an error, returns it immediately.
Otherwise, applies the function to the success value.
## Examples
iex> Result.and_then({:ok, 10}, fn x -> {:ok, x * 2} end)
{:ok, 20}
iex> Result.and_then({:error, :not_found}, fn x -> {:ok, x * 2} end)
{:error, :not_found}
"""
@spec and_then(t(a, e), (a -> t(b, e))) :: t(b, e) when a: var, b: var, e: var
def and_then({:ok, value}, fun), do: fun.(value)
def and_then({:error, _} = error, _fun), do: error
@doc """
Unwraps a result, returning the success value or a default on error.
## Examples
iex> Result.unwrap_or({:ok, 42}, 0)
42
iex> Result.unwrap_or({:error, :not_found}, 0)
0
"""
@spec unwrap_or(t(a, term()), a) :: a when a: var
def unwrap_or({:ok, value}, _default), do: value
def unwrap_or({:error, _}, default), do: default
@doc """
Checks if the result is a success.
## Examples
iex> Result.ok?({:ok, 42})
true
iex> Result.ok?({:error, :not_found})
false
"""
@spec ok?(t(term(), term())) :: boolean()
def ok?({:ok, _}), do: true
def ok?({:error, _}), do: false
@doc """
Checks if the result is an error.
## Examples
iex> Result.error?({:error, :not_found})
true
iex> Result.error?({:ok, 42})
false
"""
@spec error?(t(term(), term())) :: boolean()
def error?({:ok, _}), do: false
def error?({:error, _}), do: true
end