defmodule BACnet.Internal do @moduledoc false require Logger @compile {:inline, intify: 1} @in_library Mix.Project.get() == BACstack.MixProject and Mix.env() in [:dev, :test] @doc """ Checks whether the given argument is `Process.dest()`. """ @spec is_dest(term()) :: Macro.t() defguard is_dest(dest) when is_pid(dest) or is_port(dest) or is_atom(dest) or (is_tuple(dest) and tuple_size(dest) == 2 and is_atom(elem(dest, 0)) and is_atom(elem(dest, 1))) @doc """ Checks whether the given argument is `GenServer.server()`, it also includes the `{:via, mod, term}` format, as `GenServer.call/3` uses `GenServer.whereis/1` to resolve the server argument. """ @spec is_server(term()) :: Macro.t() defguard is_server(server) when is_pid(server) or is_atom(server) or (is_tuple(server) and tuple_size(server) == 2 and is_atom(elem(server, 0)) and is_atom(elem(server, 1))) or (is_tuple(server) and tuple_size(server) == 3 and elem(server, 0) == :via and is_atom(elem(server, 1))) @doc """ Returns the AST of the given `do` block, if this is not run as a dependency (this is the currently active Mix project being worked on). """ if @in_library do defmacro do_if_in_library(do: ast) do ast end else defmacro do_if_in_library(do: _ast) do nil end end @doc """ Returns the AST of the given `do` block, if this is not run as a dependency (this is the currently active Mix project being worked on) or debugging is enabled (Logger level `:debug` and app env `:bacstack, :debug` set to `true`. """ if @in_library or (Logger.level() == :debug and Application.compile_env(:bacstack, :debug, false)) do defmacro do_if_in_library_or_debugging(do: ast) do ast end else defmacro do_if_in_library_or_debugging(do: _ast) do nil end end # Enable debug logging (through this module) if # - this library is the app [OR] # - Logger.level() = :debug and :debug = true @doc false if @in_library or (Logger.level() == :debug and Application.compile_env(:bacstack, :debug, false)) do defmacro log_debug(message_or_fun) do quote do # credo:disable-for-next-line Credo.Check.Warning.MissedMetadataKeyInLoggerConfig Logger.debug(unquote(message_or_fun), source: __MODULE__) end end else defmacro log_debug(message_or_fun) do # We still need to inject the message_or_fun into the AST # to avoid getting unused warnings from the compiler # (wrap into an anonymous function, just like Logger) quote generated: true do _unused = fn -> unquote(message_or_fun) end :ok end end end @doc """ If the current Mix environment is equal to the `env` argument, then insert the `do:` block into the AST. """ @spec is_env(atom(), do: Macro.t()) :: Macro.t() defmacro is_env(env, do: code), do: if(Mix.env() == env, do: code) @doc """ If the current Mix environment is not equal to the `env` argument, then insert the `do:` block into the AST. """ @spec unless_env(atom(), do: Macro.t()) :: Macro.t() defmacro unless_env(env, do: code), do: if(Mix.env() != env, do: code) # Generate function clause with head pattern matching # For tuples with size 2-64 (step: 1 elements), use pattern matching # For all other tuples, use the fallback with Enum.reduce/3 # credo:disable-for-lines:20 Credo.Check.Warning.UnsafeToAtom for len <- 2..64//1 do tuple = 1..len |> Enum.map(fn val -> Macro.var(String.to_atom("v#{val}"), __MODULE__) end) |> then(&{:{}, [], &1}) block = 1..len//1 |> Stream.with_index() |> Enum.map(fn {val, index} -> quote do Bitwise.bsl( intify(unquote(Macro.var(String.to_atom("v#{val}"), __MODULE__))), unquote(abs(index)) ) end end) |> Enum.reduce(nil, fn calc, nil -> calc calc, acc -> quote do unquote(calc) + unquote(acc) end end) def tuple_to_int(unquote(tuple)) do unquote(block) end end @doc """ Calculates the integer bitmask of a tuple of booleans. """ @spec tuple_to_int(tuple()) :: non_neg_integer() def tuple_to_int(tuple) when is_tuple(tuple) and tuple_size(tuple) > 0 do Enum.reduce(0..(tuple_size(tuple) - 1)//1, 0, fn index, acc -> Bitwise.bsl(intify(elem(tuple, index)), index) + acc end) end @spec intify(boolean()) :: 0 | 1 defp intify(true), do: 1 defp intify(false), do: 0 defp intify(_term), do: raise(ArgumentError, "Element is not a boolean") @doc """ Prints a compile-time warning. This macro uses `:elixir_errors.print_warning/3` (Elixir internal API) behind the scene and allows easy function swapping in the future. """ @spec print_compile_warning(String.t()) :: Macro.t() defmacro print_compile_warning(message) do quote bind_quoted: [file: __CALLER__.file, line: __CALLER__.line, message: message] do :elixir_errors.print_warning(line, file, message) end end end