Current section

Files

Jump to
backwater src backwater_module_exposure.erl
Raw

src/backwater_module_exposure.erl

%% Copyright (c) 2017-2026 Guilherme Andrade <backwater@gandrade.net>
%%
%% Permission is hereby granted, free of charge, to any person obtaining a
%% copy of this software and associated documentation files (the "Software"),
%% to deal in the Software without restriction, including without limitation
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%% and/or sell copies of the Software, and to permit persons to whom the
%% Software is furnished to do so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included in
%% all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%% DEALINGS IN THE SOFTWARE.
-module(backwater_module_exposure).
-ifdef(E48).
-moduledoc false.
-endif.
-include("backwater_common.hrl").
%% ------------------------------------------------------------------
%% API Function Exports
%% ------------------------------------------------------------------
-export([module_name/1]).
-export([interpret_list/1]).
-export([metadata_export_list/0]).
-ignore_xref([metadata_export_list/0]).
-dialyzer({nowarn_function, metadata_export_list/0}).
%% ------------------------------------------------------------------
%% Macro Definitions
%% ------------------------------------------------------------------
-define(DEFAULT_MODULE_EXPORTS, all).
%% ------------------------------------------------------------------
%% Type Definitions
%% ------------------------------------------------------------------
-type content_type() :: {nonempty_binary(), nonempty_binary()}.
-export_type([content_type/0]).
-type exports() :: #{fun_arity_pair() => fun_properties()}.
-export_type([exports/0]).
-type t() :: module() | {module(), [opt()]}.
-export_type([t/0]).
-type opt() :: {exports, all | [{atom(), arity()}]}.
-export_type([opt/0]).
-type fun_arity_pair() :: {binary(), arity()}.
-export_type([fun_arity_pair/0]).
-type fun_properties() ::
#{
known_content_types := [content_type(), ...],
function_ref := fun()
}.
-export_type([fun_properties/0]).
-type lookup_result() :: {true, {BinModule :: nonempty_binary(), module_info()}} | false.
-export_type([lookup_result/0]).
-type module_info() :: #{exports := exports()}.
-export_type([module_info/0]).
-type raw_module_info() :: [{atom(), term()}].
%% ------------------------------------------------------------------
%% API Function Definitions
%% ------------------------------------------------------------------
-spec module_name(t()) -> module().
module_name({Module, _Opts}) ->
Module;
module_name(Module) ->
Module.
-spec interpret_list([t()]) -> #{BinModule :: nonempty_binary() => module_info()}.
interpret_list(ExposedModules) ->
KvList = lists:filtermap(fun find_and_parse_module_info/1, ExposedModules),
maps:from_list(KvList).
-spec metadata_export_list() -> [{atom(), arity()}].
metadata_export_list() ->
% faux backwater export attribute in Elixir modules (legacy)
[
{backwater_export, 0},
% callbacks
{behaviour_info, 1},
% Erlang module info
{module_info, 0},
% Erlang module info
{module_info, 1},
% Elixir module info
{'__info__', 1}
].
%% ------------------------------------------------------------------
%% Internal Function Definitions
%% ------------------------------------------------------------------
-spec find_and_parse_module_info(t()) -> lookup_result().
find_and_parse_module_info(ExposedModule) ->
Module = module_name(ExposedModule),
case find_module_info(Module) of
{ok, RawModuleInfo} ->
BackwaterExports = determine_module_exports(ExposedModule, RawModuleInfo),
FilteredBackwaterExports =
maps:filter(
fun({BinFunction, Arity}, _Info) ->
Function = binary_to_existing_atom(BinFunction, utf8),
not lists:member({Function, Arity}, metadata_export_list())
end,
BackwaterExports
),
BinModule = atom_to_binary(Module, utf8),
{true, {BinModule, #{exports => FilteredBackwaterExports}}};
error ->
false
end.
-spec exposed_module_opts(t()) -> [opt()].
exposed_module_opts({_Module, Opts}) ->
Opts;
exposed_module_opts(_Module) ->
[].
-spec find_module_info(module()) -> {ok, raw_module_info()} | error.
find_module_info(Module) ->
try
{ok, Module:module_info()}
catch
error:undef -> error
end.
-spec determine_module_exports(t(), raw_module_info()) -> exports().
determine_module_exports(ExposedModule, RawModuleInfo) ->
Module = module_name(ExposedModule),
Opts = exposed_module_opts(ExposedModule),
{exports, AtomKeyedExports} = lists:keyfind(exports, 1, RawModuleInfo),
case proplists:get_value(exports, Opts, ?DEFAULT_MODULE_EXPORTS) of
all ->
maps:from_list([backwater_export_entry_pair(Module, Pair) || Pair <- AtomKeyedExports]);
List when is_list(List) ->
maps:from_list(
[
backwater_export_entry_pair(Module, Pair)
|| Pair <- AtomKeyedExports,
lists:member(Pair, List)
]
)
end.
-spec backwater_export_entry_pair(module(), {atom(), arity()}) ->
{fun_arity_pair(), fun_properties()}.
backwater_export_entry_pair(Module, {AtomF, A}) ->
% XXX if we ever want to support custom marshalling (e.g. JSON),
% this would be a good point to start
Properties =
#{
known_content_types => [{<<"application">>, <<"x-erlang-etf">>}],
function_ref => fun Module:AtomF/A
},
F = atom_to_binary(AtomF, utf8),
{{F, A}, Properties}.