Current section
Files
Jump to
Current section
Files
lib/extensions/external_resources.ex
defmodule BridgeEx.Extensions.ExternalResources do
@moduledoc """
Preload a set of resources marking them as "external" and provide the related getter functions.
## Options
* `resources` (required): enumerable of resource names (atoms) and their paths (strings).
Each path is assumed to be relative to the module directory.
* `includes` (optional): enumerable of paths (strings) to files that should be prepended to every resource.
Each path is assumed to be relative to the module directory.
## Examples
```elixir
defmodule MyBridge do
use BridgeEx.Extensions.ExternalResources,
resources: [
my_query: "queries/query.graphql",
my_mutation: "mutations/mutation.graphql"
]
# it generates the following code:
@external_resource "\#{__DIR__}/queries/query.graphql"
@external_resource "\#{__DIR__}/mutations/mutation.graphql"
@spec my_query() :: String.t()
def my_query, do: Map.fetch!(external_resources(), :my_query)
@spec my_mutation() :: String.t()
def my_mutation, do: Map.fetch!(external_resources(), :my_mutation)
defp external_resources do
%{
my_query: "contents of query.graphql",
my_mutation: "contents of mutation.graphql"
}
end
end
```
"""
defmacro __using__(opts) do
{resources_opt, opts} = Keyword.pop(opts, :resources)
if not is_list(resources_opt) do
raise ArgumentError,
"""
resources argument in "use #{__MODULE__}" is mandatory and must be a list, got: #{inspect(resources_opt)}
"""
end
{includes_opt, opts} = Keyword.pop(opts, :includes, [])
if not is_list(includes_opt) do
raise ArgumentError,
"""
includes argument in "use #{__MODULE__}" must be a list, got: #{inspect(includes_opt)}
"""
end
if not Enum.empty?(opts) do
raise ArgumentError,
"""
got unexpected argument in "use #{__MODULE__}": #{inspect(opts)}
"""
end
if is_nil(__CALLER__.module) do
raise ArgumentError,
"""
"use #{__MODULE__}" must be called inside a module
"""
end
includes_section = Enum.map_join(includes_opt, "\n", &read_ext_res!(__CALLER__, &1))
resources =
Enum.map(resources_opt, fn {name, path} ->
content = read_ext_res!(__CALLER__, path)
if includes_section == "" do
{name, content}
else
{name, "#{includes_section}\n#{content}"}
end
end)
quote bind_quoted: [resources: resources, callback: __MODULE__] do
if not Module.has_attribute?(__MODULE__, :bridge_ex_external_resources) do
Module.register_attribute(__MODULE__, :bridge_ex_external_resources, accumulate: true)
Module.put_attribute(__MODULE__, :before_compile, callback)
end
Module.put_attribute(__MODULE__, :bridge_ex_external_resources, resources)
end
end
defmacro __before_compile__(%Macro.Env{} = env) do
resources_list =
env.module
|> Module.get_attribute(:bridge_ex_external_resources, [])
|> List.flatten()
resources_map =
Enum.reduce(resources_list, %{}, fn {name, content}, map ->
if Map.has_key?(map, name) do
raise ArgumentError, """
resource names must be unique in "use #{__MODULE__}", got duplicate: #{inspect(name)}
"""
end
Map.put(map, name, content)
end)
getters =
for {name, _content} <- resources_map do
quote do
@spec unquote(name)() :: String.t()
def unquote(name)(), do: Map.fetch!(external_resources(), unquote(name))
end
end
quote generated: true do
@spec external_resources :: %{atom => String.t()}
defp external_resources, do: unquote(Macro.escape(resources_map))
unquote_splicing(getters)
end
end
@spec read_ext_res!(Macro.Env.t(), rel_path :: Path.t()) :: binary
defp read_ext_res!(%Macro.Env{file: file, module: module}, rel_path) do
path = file |> Path.dirname() |> Path.join(rel_path)
content = File.read!(path)
Module.put_attribute(module, :external_resource, path)
content
end
end