Packages

Utility functions for the Matrix.org communications protocol

Current section

Files

Jump to
polyjuice_util lib polyjuice util client_error.ex
Raw

lib/polyjuice/util/client_error.ex

# Copyright 2021 Nicolas Jouanin <nico@beerfactory.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
defmodule Polyjuice.Util.ClientError do
@moduledoc """
Matrix Client API error mapping to Elixir
"""
defprotocol ErrorSpec do
@spec to_map(struct()) :: map()
def to_map(entity)
end
@doc """
`deferror` macro is used to define error mappings.
example:
Defining a mapping to M_FORBIDDEN:
```
> import Polyjuice.Util.ClientError
> deferror(MForbidden)
> e = MForbidden.new
%MForbidden{additional_fields: nil, errcode: "M_FORBIDDEN", error: nil}
> json_encode(e)
{:ok, "{\"errcode\":\"M_FORBIDDEN\"}"}
```
Defining a mapping to M_LIMIT_EXCEEDED with default message:
```
> import Polyjuice.Util.ClientError
> deferror(MLimitExceeded, "Too many requests")
> e = MLimitExceeded.new
%MLimitExceeded{
additional_fields: nil,
errcode: "M_LIMIT_EXCEEDED",
error: "Too many requests"
}
> json_encode(e)
{:ok, "{\"errcode\":\"M_LIMIT_EXCEEDED\",\"error\":\"Too many requests\"}"}
```
Defining a mapping to M_LIMIT_EXCEEDED with additional fields:
```
> import Polyjuice.Util.ClientError
> deferror(MLimitExceeded, "Too many requests")
> e = MLimitExceeded.new(nil, retry_after_ms: 2000)
%MLimitExceeded{
additional_fields: [retry_after_ms: 2000],
errcode: "M_LIMIT_EXCEEDED",
error: "Too many requests"
}
> json_encode(e)
{:ok,
"{\"errcode\":\"M_LIMIT_EXCEEDED\",\"error\":\"Too many requests\",\"retry_after_ms\":2000}"}
```
"""
defmacro deferror(error_name, macro_error \\ nil, macro_additional_fields \\ nil) do
module_name =
quote do
if is_nil(__MODULE__) do
String.to_atom("Elixir." <> Macro.to_string(unquote(error_name)))
else
unquote(error_name)
end
end
cap_err_code =
quote do
unquote(error_name)
|> Macro.to_string()
# take only the last component from the module hierarchy
|> String.split(".")
|> Enum.reduce(fn elem, _ -> elem end)
# split capital letters from lower case letters
|> String.split(~r(\p{Lu}), include_captures: true)
# the first element will be "" (we assume the name starts with a
# capital letter), so ignore it
|> Enum.drop(1)
# join capital letters with the lower case letters that follow it
|> Enum.chunk_every(2)
|> Enum.map(fn x -> Enum.join(x) end)
# turn words INTO_THIS
|> Enum.join("_")
|> String.upcase()
end
quote do
defmodule unquote(module_name) do
@type t :: %__MODULE__{
errcode: String.t(),
error: String.t() | nil,
additional_fields:
[{String.t() | atom, map}] | %{required(String.t()) => map} | nil
}
@enforce_keys [:errcode]
defstruct [
:errcode,
:error,
:additional_fields
]
def new(error \\ nil, additional_fields \\ nil)
when (is_binary(error) or error == nil) and
(is_list(additional_fields) or additional_fields == nil) do
%unquote(error_name){
errcode: unquote(cap_err_code),
error: if(error == nil, do: unquote(macro_error), else: error),
additional_fields:
if(additional_fields == nil,
do: unquote(macro_additional_fields),
else: additional_fields
)
}
end
end
defimpl Polyjuice.Util.ClientError.ErrorSpec, for: unquote(error_name) do
def to_map(entity = %unquote(error_name){additional_fields: additional_fields}) do
entity
|> Map.take([:errcode, :error])
|> Enum.concat(if(!is_nil(additional_fields), do: additional_fields, else: []))
|> Enum.filter(fn {key, value} -> !is_nil(value) end)
|> Map.new()
end
end
defimpl Jason.Encoder, for: unquote(error_name) do
def encode(value, opts) do
Jason.Encode.map(ErrorSpec.to_map(value), opts)
end
end
end
end
@spec json_encode(Polyjuice.Util.ClientError.ErrorSpec.t()) :: {:ok, String.t()} | {:error, any}
def json_encode(entity) do
ErrorSpec.to_map(entity) |> Jason.encode()
end
@spec json_encode!(Polyjuice.Util.ClientError.ErrorSpec.t()) :: String.t()
def json_encode!(entity) do
ErrorSpec.to_map(entity) |> Jason.encode!()
end
@spec json_encode_to_iodata(Polyjuice.Util.ClientError.ErrorSpec.t()) ::
{:ok, iodata} | {:error, any}
def json_encode_to_iodata(entity) do
ErrorSpec.to_map(entity) |> Jason.encode_to_iodata()
end
@spec json_encode_to_iodata!(Polyjuice.Util.ClientError.ErrorSpec.t()) :: iodata
def json_encode_to_iodata!(entity) do
ErrorSpec.to_map(entity) |> Jason.encode_to_iodata!()
end
end