1defmodule SMPPEX.Pdu.Oserl do
2  @moduledoc """
3  Module for converting SMPPEX Pdu structs to format used by [Oserl](https://github.com/iamaleksey/oserl) library.
4
5  """
6
7  alias SMPPEX.Pdu
8  alias SMPPEX.Pdu.Oserl, as: OserlPdu
9  alias SMPPEX.Pdu.NetworkErrorCode
10  alias SMPPEX.Protocol.TlvFormat
11
12  require Record
13  Record.defrecord(:network_error_code, type: 0, error: 0)
14
15  @type t :: {
16          command_id :: non_neg_integer,
17          command_status :: non_neg_integer,
18          sequence_number :: non_neg_integer,
19          [{field_name :: atom, field_value :: term}]
20        }
21
22  @spec to(pdu :: Pdu.t()) :: OserlPdu.t()
23
24  @doc """
25  Converts SMPPEX Pdu to Oserl format.
26
27  Unknown optional values are ignored, since Oserl stores them by symbolic names.
28  """
29  def to(pdu) do
30    {
31      Pdu.command_id(pdu),
32      Pdu.command_status(pdu),
33      Pdu.sequence_number(pdu),
34      fields_to_list(pdu)
35    }
36  end
37
38  @spec from(oserl_pdu :: OserlPdu.t()) :: Pdu.t()
39
40  @doc """
41  Converts PDU from Oserl format to SMPPEX Pdu.
42  """
43  def from({command_id, command_status, sequence_number, field_list} = _oserl_pdu) do
44    converted_field_list =
45      field_list
46      |> Enum.map(&preprocess/1)
47      |> Enum.reject(&is_nil/1)
48      |> Enum.map(&list_to_string/1)
49
50    {mandatory, optional} = list_to_fields(converted_field_list, %{}, %{})
51
52    Pdu.new(
53      {command_id, command_status, sequence_number},
54      mandatory,
55      optional
56    )
57  end
58
59  defp fields_to_list(pdu) do
60    (pdu |> Pdu.mandatory_fields() |> Map.to_list() |> Enum.map(&string_to_list(&1))) ++
61      (pdu |> Pdu.optional_fields() |> Map.to_list() |> ids_to_names)
62  end
63
64  defp list_to_fields([], mandatory, optional), do: {mandatory, optional}
65
66  defp list_to_fields([{name, value} | list], mandatory, optional) do
67    case kind(name) do
68      {:optional, id} -> list_to_fields(list, mandatory, Map.put(optional, id, value))
69      :mandatory -> list_to_fields(list, Map.put(mandatory, name, value), optional)
70    end
71  end
72
73  defp kind(id) when is_integer(id), do: {:optional, id}
74
75  defp kind(name) when is_atom(name) do
76    case TlvFormat.id_by_name(name) do
77      {:ok, id} -> {:optional, id}
78      :unknown -> :mandatory
79    end
80  end
81
82  defp ids_to_names(by_ids, by_names \\ [])
83
84  defp ids_to_names([], by_names), do: by_names
85
86  defp ids_to_names([{name, value} | by_ids], by_names) when is_atom(name),
87    do: ids_to_names(by_ids, [{name, value} | by_names])
88
89  defp ids_to_names([{id, value} | by_ids], by_names) when is_integer(id) do
90    case TlvFormat.name_by_id(id) do
91      {:ok, name} -> ids_to_names(by_ids, [string_to_list({name, value}) | by_names])
92      :unknown -> ids_to_names(by_ids, [{id, value} | by_names])
93    end
94  end
95
96  defp list_to_string({key, value}) when is_list(value), do: {key, :erlang.list_to_binary(value)}
97  defp list_to_string({key, value}), do: {key, value}
98
99  defp string_to_list({key, value}) when is_binary(value),
100    do: {key, :erlang.binary_to_list(value)}
101
102  defp string_to_list({key, value}), do: {key, value}
103
104  defp preprocess({:network_error_code, network_error_code(type: type_code, error: error_code)}),
105    do: {:network_error_code, NetworkErrorCode.encode(type_code, error_code)}
106
107  defp preprocess({:network_error_code, []}), do: nil
108  defp preprocess({key, value}), do: {key, value}
109end
110