%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0. If a copy of the MPL was not distributed with this
%% file, You can obtain one at https://mozilla.org/MPL/2.0/.
%%
%% Copyright © 2021-2022 VMware, Inc. or its affiliates. All rights reserved.
%%
%% @doc Khepri payloads.
%%
%% Payloads are the structure used to attach something to a tree node in the
%% store. Khepri supports the following payloads:
%% <ul>
%% <li>No payload at all ({@link no_payload()}</li>
%% <li>Data payload used to store any Erlang term ({@link data()})</li>
%% <li>Stored procedure payload used to store functions ({@link sproc()})</li>
%% </ul>
%%
%% Usually, there is no need to explicitly use this module as the type of
%% payload will be autodetected, thanks to the {@link wrap/1} function already
%% called internally.
-module(khepri_payload).
-include("src/khepri_error.hrl").
-include("src/khepri_fun.hrl").
-include("src/khepri_payload.hrl").
-export([none/0,
data/1,
sproc/1,
wrap/1,
prepare/1]).
-type no_payload() :: ?NO_PAYLOAD.
%% Internal value used to mark that a tree node has no payload attached.
-type data() :: #p_data{}.
%% Internal structure to wrap any Erlang term before it can be stored in a
%% tree node.
%%
%% The only constraint is the conversion to an Erlang binary must be supported
%% by this term.
-type sproc() :: #p_sproc{}.
%% Internal structure to wrap an anonymous function before it can be stored in
%% a tree node and later executed.
-type payload() :: no_payload() | data() | sproc().
%% All types of payload stored in the nodes of the tree structure.
%%
%% Beside the absence of payload, the only type of payload supported is data.
-export_type([no_payload/0,
data/0,
sproc/0,
payload/0]).
-spec none() -> no_payload().
%% @doc Returns the internal value used to mark that a tree node has no
%% payload attached.
%%
%% @see no_payload().
none() ->
?NO_PAYLOAD.
-spec data(Term) -> Payload when
Term :: khepri:data(),
Payload :: data().
%% @doc Returns the same term wrapped into an internal structure ready to be
%% stored in the tree.
%%
%% @see data().
data(Term) ->
#p_data{data = Term}.
-spec sproc(Fun) -> Payload when
Fun :: khepri_fun:standalone_fun() | fun(),
Payload :: sproc().
%% @doc Returns the same function wrapped into an internal structure ready to
%% be stored in the tree.
%%
%% @see sproc().
sproc(Fun) when is_function(Fun) ->
#p_sproc{sproc = Fun,
%% This will be overridden with the correct value when the
%% function will be extracted.
is_valid_as_tx_fun = false};
sproc(#standalone_fun{} = Fun) ->
#p_sproc{sproc = Fun,
is_valid_as_tx_fun = false}.
-spec wrap(Payload) -> WrappedPayload when
Payload :: payload() | khepri:data() | fun(),
WrappedPayload :: payload().
%% @doc Automatically detects the payload type and ensures it is wrapped in
%% one of the internal types.
%%
%% The internal types make sure we avoid any collision between any
%% user-provided terms and internal structures.
%%
%% @param Payload an already wrapped payload, or any term which needs to be
%% wrapped.
%%
%% @returns the wrapped payload.
wrap(Payload) when ?IS_KHEPRI_PAYLOAD(Payload) -> Payload;
wrap(Fun) when is_function(Fun) -> sproc(Fun);
wrap(Data) -> data(Data).
-spec prepare(Payload) -> Payload when
Payload :: payload().
%% @doc Finishes any needed changes to the payload before it is ready to be
%% stored.
%%
%% This currently only includes the conversion of anonymous functions to
%% standalone functions for stored procedures' payload records.
%%
%% @private
prepare(?NO_PAYLOAD = Payload) ->
Payload;
prepare(#p_data{} = Payload) ->
Payload;
prepare(#p_sproc{sproc = Fun} = Payload)
when is_function(Fun) ->
try
case khepri_tx_adv:to_standalone_fun(Fun, auto) of
#standalone_fun{} = StandaloneFun1 ->
Payload#p_sproc{sproc = StandaloneFun1,
is_valid_as_tx_fun = rw};
_ ->
%% TODO: Improve `khepri_fun' API to avoid a second
%% extraction.
StandaloneFun1 = khepri_tx_adv:to_standalone_fun(Fun, rw),
Payload#p_sproc{sproc = StandaloneFun1,
is_valid_as_tx_fun = ro}
end
catch
error:?khepri_exception(failed_to_prepare_tx_fun, _) ->
%% TODO: Prevent the use of `khepri_tx' and `khepri_tx_adv' in
%% regular stored procedures.
StandaloneFun2 = khepri_sproc:to_standalone_fun(Fun),
Payload#p_sproc{sproc = StandaloneFun2,
is_valid_as_tx_fun = false}
end.