# Copyright 2021 Nicolas Jouanin # # 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