towerops/lib/snmpkit/snmp_lib/error.ex
Graham McIntire 348975dcc9 Rewrite polling_offset, changelog_parser, and user_agent_parser in Gleam (#103)
Migrate three pure-function modules from Elixir to Gleam:

- polling_offset: deterministic hash-based polling offset (pure Gleam, no FFI)
- changelog_parser: text parsing logic in Gleam, thin Elixir wrapper for File I/O
- user_agent_parser: regex-based UA parsing in Gleam with Erlang FFI for
  atom-keyed map conversion

Add gleam_regexp dependency for regex support in Gleam modules.
Update all call sites and tests to use Gleam module atoms.

Reviewed-on: graham/towerops-web#103
2026-03-21 10:11:06 -05:00

165 lines
4.4 KiB
Elixir

defmodule SnmpKit.SnmpLib.Error do
@moduledoc """
Standard SNMP error handling and error code utilities.
Core logic is implemented in Gleam (`:snmpkit@snmp_lib@error`).
This wrapper provides the idiomatic Elixir API that accepts atoms,
integers, and strings as error status identifiers.
"""
@gleam :snmpkit@snmp_lib@error
@ffi :snmpkit_error_ffi
@type error_status ::
:no_error
| :too_big
| :no_such_name
| :bad_value
| :read_only
| :gen_err
| non_neg_integer()
@type error_index :: non_neg_integer()
@type varbind :: {list(), any()}
@type varbinds :: [varbind()]
## Standard Error Code Constants
@spec no_error() :: 0
def no_error, do: 0
@spec too_big() :: 1
def too_big, do: 1
@spec no_such_name() :: 2
def no_such_name, do: 2
@spec bad_value() :: 3
def bad_value, do: 3
@spec read_only() :: 4
def read_only, do: 4
@spec gen_err() :: 5
def gen_err, do: 5
## Error Utilities
@spec error_name(error_status()) :: String.t()
def error_name(code) when is_integer(code) do
code |> @gleam.from_code() |> @gleam.error_name()
end
def error_name(atom) when is_atom(atom) do
atom |> @ffi.atom_to_error_status() |> @gleam.error_name()
end
@spec error_atom(error_status()) :: atom()
def error_atom(code) when is_integer(code) do
code |> @gleam.from_code() |> @ffi.error_status_to_atom()
end
def error_atom(atom) when is_atom(atom), do: atom
@spec error_code(atom() | String.t()) :: non_neg_integer()
def error_code(atom) when is_atom(atom) do
status = @ffi.atom_to_error_status(atom)
code = @gleam.error_code(status)
# UnknownError returns -1 from Gleam; map to gen_err (5) for compatibility
if code == -1, do: 5, else: code
end
def error_code(name) when is_binary(name) do
status = @gleam.from_name(name)
code = @gleam.error_code(status)
if code == -1, do: 5, else: code
end
def error_code(_), do: 5
@spec format_error(error_status(), error_index(), varbinds()) :: String.t()
def format_error(error_status, error_index, varbinds \\ []) do
status = to_gleam_status(error_status)
@gleam.format_error(status, error_index, varbinds)
end
@spec retriable_error?(error_status()) :: boolean()
def retriable_error?(error_status) do
error_status |> to_gleam_status() |> @gleam.is_retriable()
end
@spec create_error_response(map(), error_status(), error_index()) ::
{:ok, map()} | {:error, atom()}
def create_error_response(request_pdu, error_status, error_index) do
case validate_request_pdu(request_pdu) do
:ok ->
error_code_num =
if is_integer(error_status), do: error_status, else: error_code(error_status)
error_response = %{
type: :get_response,
request_id: Map.get(request_pdu, :request_id, 0),
error_status: error_code_num,
error_index: error_index,
varbinds: Map.get(request_pdu, :varbinds, [])
}
{:ok, error_response}
{:error, reason} ->
{:error, reason}
end
end
@spec valid_error_status?(any()) :: boolean()
def valid_error_status?(status) when is_integer(status) do
@gleam.is_valid_code(status)
end
def valid_error_status?(status) when is_atom(status) do
error_name(status) != "unknown_error"
end
def valid_error_status?(_), do: false
@spec all_error_codes() :: [non_neg_integer()]
def all_error_codes, do: @gleam.all_codes()
@spec all_error_atoms() :: [atom()]
def all_error_atoms do
[
:no_error,
:too_big,
:no_such_name,
:bad_value,
:read_only,
:gen_err,
:no_access,
:wrong_type,
:wrong_length,
:wrong_encoding,
:wrong_value,
:no_creation,
:inconsistent_value,
:resource_unavailable,
:commit_failed,
:undo_failed,
:authorization_error,
:not_writable,
:inconsistent_name
]
end
@spec error_severity(error_status()) :: :info | :warning | :error
def error_severity(error_status) do
error_status |> to_gleam_status() |> @gleam.severity() |> String.to_atom()
end
## Private Helpers
defp to_gleam_status(code) when is_integer(code), do: @gleam.from_code(code)
defp to_gleam_status(atom) when is_atom(atom), do: @ffi.atom_to_error_status(atom)
defp validate_request_pdu(request_pdu) when is_map(request_pdu), do: :ok
defp validate_request_pdu(_), do: {:error, :invalid_request_pdu}
end