defmodule SMPPEX.Pdu do
@moduledoc """
Module for working with Pdu struct representing parsed SMPP PDU
"""
alias SMPPEX.Protocol.TlvFormat
alias SMPPEX.Protocol.CommandNames
alias SMPPEX.Pdu
alias SMPPEX.RawPdu
@type t :: %Pdu{
command_id: non_neg_integer,
command_status: non_neg_integer,
sequence_number: non_neg_integer,
ref: reference,
mandatory: map,
optional: map
}
defstruct command_id: 0,
command_status: 0,
sequence_number: 0,
ref: nil,
mandatory: %{},
optional: %{}
@type header :: {non_neg_integer, non_neg_integer, non_neg_integer} | non_neg_integer
@spec new(header, map, map) :: t
@doc """
Construct a new Pdu from header, mandatory fields and optional(TLV) fields.
Header may be either an integer, then it is treated as command id,
or a tuple `{command_id, command_status, sequence_number}`
Each Pdu is created with a unique ref field, by which one can later
trace Pdu's identity.
## Examples
iex(1)> SMPPEX.Pdu.new(1)
%SMPPEX.Pdu{command_id: 1, command_status: 0, mandatory: %{}, optional: %{},
ref: #Reference<0.0.3.215>, sequence_number: 0}
iex(2)> SMPPEX.Pdu.new({1, 0, 123}, %{system_id: "sid", password: "pass"}, %{})
%SMPPEX.Pdu{command_id: 1, command_status: 0,
mandatory: %{password: "pass", system_id: "sid"}, optional: %{},
ref: #Reference<0.0.3.219>, sequence_number: 123}
"""
def new(header, mandatory_fields \\ %{}, optional_fields \\ %{}) do
case header do
c_id when is_integer(c_id) ->
%Pdu{
command_id: c_id,
ref: make_ref(),
mandatory: mandatory_fields,
optional: optional_fields
}
{c_id, c_status, s_number} ->
%Pdu{
command_id: c_id,
command_status: c_status,
sequence_number: s_number,
ref: make_ref(),
mandatory: mandatory_fields,
optional: optional_fields
}
end
end
@spec command_name(t) :: atom
@doc """
Returns Pdu's symbolic command name as an atom or `:unknown` if Pdu's `command_id`
do not correspond to any real SMPP command.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(1)
iex(2)> SMPPEX.Pdu.command_name(pdu)
:bind_receiver
iex(3)> pdu = SMPPEX.Pdu.new(1111111)
iex(4)> SMPPEX.Pdu.command_name(pdu)
:unknown
"""
def command_name(pdu) do
case CommandNames.name_by_id(pdu.command_id) do
{:ok, name} -> name
:unknown -> :unknown
end
end
@spec command_id(t) :: non_neg_integer
@doc """
Returns Pdu's `command_id`.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(1)
iex(2)> SMPPEX.Pdu.command_id(pdu)
1
"""
def command_id(pdu) do
pdu.command_id |> to_int
end
@spec command_status(t) :: non_neg_integer
@doc """
Returns Pdu's `command_status`.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new({1, 4, 123})
iex(2)> SMPPEX.Pdu.command_status(pdu)
4
"""
def command_status(pdu) do
pdu.command_status |> to_int
end
@spec sequence_number(t) :: non_neg_integer
@doc """
Returns Pdu's `sequence_number`.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new({1, 4, 123})
iex(2)> SMPPEX.Pdu.sequence_number(pdu)
123
"""
def sequence_number(pdu) do
pdu.sequence_number |> to_int
end
@doc """
Returns Pdu's unique reference `ref`.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new({1, 4, 123})
iex(2)> is_reference SMPPEX.Pdu.ref(pdu)
true
"""
def ref(pdu) do
pdu.ref
end
@spec mandatory_field(t, atom) :: term
@doc """
Get Pdu mandatory field. If Pdu does not have the field, `nil` is returned.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new({1, 4, 123}, %{system_id: "system_id"})
iex(2)> SMPPEX.Pdu.mandatory_field(pdu, :system_id)
"system_id"
iex(3)> SMPPEX.Pdu.mandatory_field(pdu, :short_message)
nil
"""
def mandatory_field(pdu, name) when is_atom(name) do
pdu.mandatory[name]
end
@spec set_mandatory_field(t, atom, any) :: t
@doc """
Sets Pdu mandatory field. New Pdu is returned.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new({1, 4, 123}, %{system_id: "system_id"})
iex(2)> pdu1 = SMPPEX.Pdu.set_mandatory_field(pdu, :password, "pass")
iex(3)> SMPPEX.Pdu.mandatory_field(pdu1, :password)
"pass"
"""
def set_mandatory_field(pdu, name, value) do
%Pdu{pdu | mandatory: Map.put(pdu.mandatory, name, value)}
end
@spec optional_field(t, integer | atom) :: any
@doc """
Get Pdu optional(TLV) field by name or by integer id. If Pdu does not have the
field or field name is unknown, `nil` is returned.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4, %{}, %{0x0424 => "hello"})
iex(2)> SMPPEX.Pdu.optional_field(pdu, :message_payload)
"hello"
iex(3)> SMPPEX.Pdu.optional_field(pdu, 0x0424)
"hello"
iex(4)> SMPPEX.Pdu.optional_field(pdu, :receipted_message_id)
nil
iex(5)> SMPPEX.Pdu.optional_field(pdu, :unknown_tlv_name)
nil
"""
def optional_field(pdu, id) when is_integer(id) do
case Map.has_key?(pdu.optional, id) do
true ->
pdu.optional[id]
false ->
case TlvFormat.name_by_id(id) do
{:ok, name} -> pdu.optional[name]
:unknown -> nil
end
end
end
def optional_field(pdu, name) when is_atom(name) do
case Map.has_key?(pdu.optional, name) do
true ->
pdu.optional[name]
false ->
case TlvFormat.id_by_name(name) do
{:ok, id} -> pdu.optional[id]
:unknown -> nil
end
end
end
@spec set_optional_field(t, integer | atom, any) :: t
@doc """
Sets Pdu optional field. New Pdu is returned.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4)
iex(2)> pdu1 = SMPPEX.Pdu.set_optional_field(pdu, :message_payload, "hello")
iex(3)> SMPPEX.Pdu.optional_field(pdu1, 0x0424)
"hello"
"""
def set_optional_field(pdu, name, value) when is_atom(name) do
optional = Map.delete(pdu.optional, name)
optional =
case TlvFormat.id_by_name(name) do
{:ok, id} -> Map.delete(optional, id)
:unknown -> optional
end
%Pdu{pdu | optional: Map.put(optional, name, value)}
end
def set_optional_field(pdu, id, value) when is_integer(id) do
optional = Map.delete(pdu.optional, id)
optional =
case TlvFormat.name_by_id(id) do
{:ok, name} -> Map.delete(optional, name)
:unknown -> optional
end
%Pdu{pdu | optional: Map.put(optional, id, value)}
end
@spec field(t, integer | atom) :: any
@doc """
Get Pdu mandatory or optional(TLV) field by name or by integer id. If Pdu does not have the
field or field name is unknown, `nil` is returned.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4, %{short_message: "hi"}, %{0x0424 => "hello"})
iex(2)> SMPPEX.Pdu.field(pdu, :message_payload)
"hello"
iex(3)> SMPPEX.Pdu.field(pdu, 0x0424)
"hello"
iex(4)> SMPPEX.Pdu.field(pdu, :short_message)
"hi"
iex(5)> SMPPEX.Pdu.field(pdu, :unknown_name)
nil
"""
def field(pdu, id) when is_integer(id) do
optional_field(pdu, id)
end
def field(pdu, name) do
mandatory_field(pdu, name) || optional_field(pdu, name)
end
@spec optional_fields(t) :: map
@doc """
Get the whole set of optional(TLV) fields as a map.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4, %{short_message: "hi"}, %{0x0424 => "hello"})
iex(2)> SMPPEX.Pdu.optional_fields(pdu)
%{0x0424 => "hello"}
"""
def optional_fields(pdu), do: pdu.optional
@spec mandatory_fields(t) :: map
@doc """
Get the whole set of mandatory fields as a map.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4, %{short_message: "hi"}, %{0x0424 => "hello"})
iex(2)> SMPPEX.Pdu.mandatory_fields(pdu)
%{short_message: "hi"}
"""
def mandatory_fields(pdu), do: pdu.mandatory
@spec same?(t, t) :: boolean
@doc """
Checks if two Pdus are copies of the same Pdu.
## Examples
iex(1)> pdu1 = SMPPEX.Pdu.new(4)
iex(2)> pdu2 = SMPPEX.Pdu.new(4)
iex(3)> SMPPEX.Pdu.same?(pdu1, pdu2)
false
iex(4)> SMPPEX.Pdu.same?(pdu1, pdu1)
true
"""
def same?(pdu1, pdu2), do: pdu1.ref != nil and pdu2.ref != nil and pdu1.ref == pdu2.ref
@spec resp?(t) :: boolean
@doc """
Checks if Pdu is a response Pdu.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4)
iex(2)> SMPPEX.Pdu.resp?(pdu)
false
iex(3)> pdu = SMPPEX.Pdu.new(0x80000004)
iex(4)> SMPPEX.Pdu.resp?(pdu)
true
"""
def resp?(pdu), do: command_id(pdu) > 0x80000000
@spec success_resp?(t) :: boolean
@doc """
Checks if Pdu is a successful response Pdu.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new({0x80000004, 1, 0})
iex(2)> SMPPEX.Pdu.success_resp?(pdu)
false
iex(3)> pdu = SMPPEX.Pdu.new({0x80000004, 0, 0})
iex(4)> SMPPEX.Pdu.success_resp?(pdu)
true
"""
def success_resp?(pdu) do
resp?(pdu) and command_status(pdu) == 0
end
@spec bind?(t) :: boolean
@doc """
Checks if Pdu is a bind request.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4)
iex(2)> SMPPEX.Pdu.bind?(pdu)
false
iex(3)> pdu = SMPPEX.Pdu.new(1)
iex(4)> SMPPEX.Pdu.bind?(pdu)
true
"""
def bind?(pdu) do
command_id = Pdu.command_id(pdu)
case CommandNames.name_by_id(command_id) do
{:ok, command_name} ->
command_name == :bind_receiver or command_name == :bind_transmitter or
command_name == :bind_transceiver
:unknown ->
false
end
end
@spec bind_resp?(t) :: boolean
@doc """
Checks if Pdu is a bind response.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(0x80000004)
iex(2)> SMPPEX.Pdu.bind_resp?(pdu)
false
iex(3)> pdu = SMPPEX.Pdu.new(0x80000001)
iex(4)> SMPPEX.Pdu.bind_resp?(pdu)
true
"""
def bind_resp?(pdu) do
command_id = Pdu.command_id(pdu)
case CommandNames.name_by_id(command_id) do
{:ok, command_name} ->
command_name == :bind_receiver_resp or command_name == :bind_transmitter_resp or
command_name == :bind_transceiver_resp
:unknown ->
false
end
end
@type addr :: {String.t(), byte, byte}
@spec source(t) :: addr
@doc """
Returns Pdu's `:source_addr`, `:source_addr_ton` and `:source_addr_npi` fields
in a tuple.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4, %{source_addr: "from", source_addr_ton: 1, source_addr_npi: 2})
iex(2)> SMPPEX.Pdu.source(pdu)
{"from", 1, 2}
"""
def source(pdu) do
fields_as_tuple(pdu, [:source_addr, :source_addr_ton, :source_addr_npi])
end
@spec dest(t) :: addr
@doc """
Returns Pdu's `:destination_addr`, `:dest_addr_ton` and `:dest_addr_npi` fields
in a tuple.
## Examples
iex(1)> pdu = SMPPEX.Pdu.new(4, %{destination_addr: "to", dest_addr_ton: 1, dest_addr_npi: 2})
iex(2)> SMPPEX.Pdu.dest(pdu)
{"to", 1, 2}
"""
def dest(pdu) do
fields_as_tuple(pdu, [:destination_addr, :dest_addr_ton, :dest_addr_npi])
end
@spec as_reply_to(pdu :: Pdu.t(), reply_to_pdu :: Pdu.t() | RawPdu.t()) :: Pdu.t()
@doc """
Makes `pdu` be reply to the `reply_to_pdu`, i.e. assigns `reply_to_pdu`'s
`sequence_number` to `pdu`.
## Examples
iex(1)> pdu1 = SMPPEX.Pdu.new({0x00000004, 0, 123})
iex(2)> pdu2 = SMPPEX.Pdu.new(0x80000004) |> SMPPEX.Pdu.as_reply_to(pdu1)
iex(3)> SMPPEX.Pdu.sequence_number(pdu2)
123
"""
def as_reply_to(pdu, reply_to_pdu) do
%Pdu{pdu | sequence_number: reply_to_pdu.sequence_number}
end
defp fields_as_tuple(pdu, fields) do
fields
|> Enum.map(fn field_name -> Pdu.field(pdu, field_name) end)
|> List.to_tuple()
end
defp to_int(val) when is_integer(val), do: val
defp to_int(_), do: 0
end