Packages

A User-Friendly Component-Based Cardano SDK for Elixir.

Current section

Files

Jump to
sutra_cardano lib sutra cardano utils.ex
Raw

lib/sutra/cardano/utils.ex

defmodule Sutra.Utils do
@moduledoc """
General utility functions for common operations.
This module provides helper functions for safe operations on lists, maps, tuples,
and result handling. All functions are designed to be safe and handle edge cases gracefully.
"""
@doc """
Returns head of list. For empty list returns nil
## Examples
iex> safe_head([])
nil
iex> safe_head([1,2,3])
1
"""
@spec safe_head(list()) :: any() | nil
def safe_head([]), do: nil
def safe_head([head | _]), do: head
@doc """
Returns tail of list. For empty list returns empty list
## Examples
iex> safe_tail([])
[]
iex> safe_tail([1, 2, 3])
[2, 3]
"""
@spec safe_tail(list()) :: list()
def safe_tail([]), do: []
def safe_tail([_ | tail]), do: tail
@doc """
Decode Base16 encoded String. Returns original string for Invalid value
## Examples
iex> safe_base16_decode("616263")
"abc"
iex> safe_base16_decode("invalid-str")
"invalid-str"
"""
@spec safe_base16_decode(any()) :: binary()
def safe_base16_decode(val) when is_binary(val) do
val
|> Base.decode16(case: :mixed)
|> ok_or(val)
end
@doc """
Returns the input value unchanged. Useful for function composition and default transforms.
## Examples
iex> identity(42)
42
iex> identity("hello")
"hello"
iex> identity([1, 2, 3])
[1, 2, 3]
"""
@spec identity(any()) :: any()
def identity(x), do: x
@doc """
Apply function by flipping arguments
## Examples
iex> flip(1, 2, fn x, y -> [x, y] end)
[2, 1]
iex> flip(2, 1, fn x, y -> [x, y] end)
[1, 2]
"""
@spec flip(any(), any(), (any(), any() -> any())) :: any()
def flip(a, b, f), do: f.(b, a)
@doc """
return default value if value is nil or empty list.
## Examples
iex> maybe(nil, [1,2,3])
[1, 2, 3]
iex> maybe([], [1,2,3])
[1, 2, 3]
iex> maybe(nil, fn -> [1, 2, 3] end)
[1, 2, 3]
iex> maybe(["a"], [1, 2, 3])
["a"]
maybe can also be used to apply function on values
## Examples
iex> maybe([1, 2, 3], nil, &Enum.join/1)
"123"
"""
@spec maybe(any(), any(), (any() -> any()) | nil) :: any()
def maybe(data, default, convertor \\ nil)
def maybe(result, arg, _) when is_nil(result) or result == [] do
if is_function(arg, 0), do: arg.(), else: arg
end
def maybe(data, _, f2) when not is_nil(data) and data !== [] and is_function(f2, 1),
do: f2.(data)
def maybe(data, _, nil), do: data
@doc """
Returns value from {:ok, value} tuple, otherwise returns default.
## Examples
iex> ok_or({:ok, "success"}, "default")
"success"
iex> ok_or({:error, "failed"}, "default")
"default"
iex> ok_or(nil, "default")
"default"
"""
@spec ok_or({:ok, any()} | any(), any()) :: any()
def ok_or({:ok, result}, _), do: result
def ok_or(_, default) when is_function(default, 0), do: default.()
def ok_or(result, default) when is_function(default, 1), do: default.(result)
def ok_or(_, default), do: default
@doc """
Safely appends value to list. Creates new list if first argument is not a list.
## Examples
iex> safe_append([1, 2], 3)
[1, 2, 3]
iex> safe_append([1, 2], [3, 4])
[1, 2, 3, 4]
iex> safe_append(nil, 3)
[3]
"""
@spec safe_append(any(), any()) :: list()
def safe_append(list, val) when is_list(list) do
new_val = if is_list(val), do: val, else: [val]
list ++ new_val
end
def safe_append(_, val), do: if(is_list(val), do: val, else: [val])
@doc """
Merges value to map key, appending to existing list values.
## Examples
iex> merge_value_to_map(%{}, :key, "value")
%{key: ["value"]}
iex> merge_value_to_map(%{key: ["old"]}, :key, "new")
%{key: ["new", "old"]}
"""
@spec merge_value_to_map(map(), any(), any()) :: map()
def merge_value_to_map(map, key, value) do
prev_val = Map.get(map, key, [])
new_val = if is_list(value), do: value, else: [value]
Map.put(map, key, new_val ++ prev_val)
end
@doc """
Filters nil values from map and adds indexes. For map values, adds index directly; for other values, wraps in indexed structure.
## Examples
iex> result = with_sorted_indexed_map(%{a: "value1", b: nil, c: "value2"})
iex> map_size(result)
2
iex> result[:a][:value]
"value1"
iex> result[:c][:value]
"value2"
iex> is_integer(result[:a][:index])
true
iex> result = with_sorted_indexed_map(%{a: %{data: "test"}, b: "simple"})
iex> result[:a][:data]
"test"
iex> result[:b][:value]
"simple"
iex> is_integer(result[:a][:index]) and is_integer(result[:b][:index])
true
"""
@spec with_sorted_indexed_map(map()) :: map()
def with_sorted_indexed_map(map) when is_map(map) do
map = Map.filter(map, fn {_k, v} -> not is_nil(v) end)
for {{k, v}, i} <- Enum.with_index(map), into: %{} do
if is_map(v) and not is_struct(v),
do: {k, Map.put(v, :index, i)},
else: {k, %{index: i, value: v}}
end
end
@doc """
Converts list to indexed map using key function. Each element gets wrapped with its index and value.
## Examples
iex> to_sorted_indexed_map(["apple", "banana", "cherry"], fn str -> String.first(str) end)
%{"a" => %{index: 0, value: "apple"}, "b" => %{index: 1, value: "banana"}, "c" => %{index: 2, value: "cherry"}}
iex> to_sorted_indexed_map([1, 2, 3], fn x -> x * 10 end)
%{10 => %{index: 0, value: 1}, 20 => %{index: 1, value: 2}, 30 => %{index: 2, value: 3}}
"""
@spec to_sorted_indexed_map(list(), (any() -> any())) :: map()
def to_sorted_indexed_map(list, key_func) when is_list(list) and is_function(key_func, 1) do
for {v, i} <- Enum.with_index(list), into: %{} do
{key_func.(v), %{index: i, value: v}}
end
end
@doc """
Removes first matching element from list and returns {remaining_list, removed_element}.
## Examples
iex> without_elem([1, 2, 3, 2], fn x -> x == 2 end)
{[1, 3, 2], 2}
iex> without_elem([1, 2, 3], fn x -> x == 5 end)
{[1, 2, 3], nil}
"""
@spec without_elem(list(), (any() -> boolean())) :: {list(), any() | nil}
def without_elem([], _), do: {[], nil}
def without_elem([h | t], func) when is_function(func, 1) do
if func.(h) do
{t, h}
else
{elems, v} = without_elem(t, func)
{[h | elems], v}
end
end
@doc """
Flattens nested lists into a single list.
## Examples
iex> merge_list([[1, 2], [3, 4]])
[1, 2, 3, 4]
iex> merge_list([])
[]
"""
@spec merge_list([list()]) :: list()
def merge_list(list), do: List.flatten(list)
@doc """
Returns first element of a tuple.
## Examples
iex> fst({1, 2})
1
iex> fst({"a", "b"})
"a"
"""
@spec fst({any(), any()}) :: any()
def fst({a, _}), do: a
@doc """
Returns second element of a tuple.
## Examples
iex> snd({1, 2})
2
iex> snd({"a", "b"})
"b"
"""
@spec snd({any(), any()}) :: any()
def snd({_, b}), do: b
@doc """
Checks if value is an instance of given struct module.
## Examples
iex> instance_of?(%URI{}, URI)
true
iex> instance_of?("string", URI)
false
"""
@spec instance_of?(any(), module()) :: boolean()
def instance_of?(v, l) when is_struct(v), do: v.__struct__ == l
def instance_of?([v | rest], l),
do: instance_of?(v, l) and (rest == [] or instance_of?(rest, l))
def instance_of?(_, _), do: false
@doc """
Applies function to value if it's an {:ok, value} tuple, otherwise returns original value.
## Examples
iex> when_ok({:ok, 5}, fn x -> x * 2 end)
10
iex> when_ok({:error, "failed"}, fn x -> x * 2 end)
{:error, "failed"}
"""
@spec when_ok({:ok, any()} | any(), (any() -> any())) :: any()
def when_ok({:ok, result}, apply) when is_function(apply, 1) do
apply.(result)
end
def when_ok(result, _), do: result
@doc """
Wraps non-error values in {:ok, value} tuple. Passes through error tuples unchanged.
## Examples
iex> ok_or_error("success")
{:ok, "success"}
iex> ok_or_error({:error, "failed"})
{:error, "failed"}
"""
@spec ok_or_error({:error, any()} | any()) :: {:ok, any()} | {:error, any()}
def ok_or_error({:error, err}), do: {:error, err}
def ok_or_error(result), do: {:ok, result}
end