From 18cdbb476f21852ea773143e9c64440a7a8dd902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20P=C3=A9dron?= Date: Tue, 3 May 2022 18:14:24 +0200 Subject: [PATCH] Introduce an Elixir-friendly API To improve the integration with a pure Elixir code base, this patch brings the following changes: * `khepri` now exposes "bang functions", i.e. functions named e.g. `get!(StoreId, PathPattern)`. Those functions return the result directly in case of success (i.e. it is not "wrapped" into an `{ok, _}` tuple), or throw an exception. * `khepri_path` provides the `~p` and `~P` sigils to offer easy path parsing without calling `khepri_path` explicitly. V2: Use `erlang:error/1` to raise exceptions in both bang functions and sigils, instead of `erlang:throw/1`. The former is the convention in Elixir. --- src/khepri.erl | 455 +++++++++++++++++++++++++++++++++++++++- src/khepri_path.erl | 46 ++++ test/bang_functions.erl | 187 +++++++++++++++++ test/sigils.erl | 38 ++++ 4 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 test/bang_functions.erl create mode 100644 test/sigils.erl diff --git a/src/khepri.erl b/src/khepri.erl index 0ee219b7..e38f6942 100644 --- a/src/khepri.erl +++ b/src/khepri.erl @@ -107,10 +107,18 @@ %% transaction functions. transaction/1, transaction/2, transaction/3, transaction/4, + 'put!'/2, 'put!'/3, 'put!'/4, 'put!'/5, + 'create!'/2, 'create!'/3, 'create!'/4, 'create!'/5, + 'update!'/2, 'update!'/3, 'update!'/4, 'update!'/5, + 'compare_and_swap!'/3, 'compare_and_swap!'/4, 'compare_and_swap!'/5, + 'compare_and_swap!'/6, + 'get!'/1, 'get!'/2, 'get!'/3, + 'delete!'/1, 'delete!'/2, 'delete!'/3, + info/0, info/1, info/2]). --compile({no_auto_import, [get/2, put/2, erase/1]}). +-compile({no_auto_import, [get/1, get/2, put/2, erase/1]}). %% FIXME: Dialyzer complains about several functions with "optional" arguments %% (but not all). I believe the specs are correct, but can't figure out how to @@ -2175,6 +2183,451 @@ clear_store(Options) when is_map(Options) -> clear_store(StoreId, Options) -> delete(StoreId, [?STAR], Options). +%% ------------------------------------------------------------------- +%% "Bang functions", mostly an Elixir convention. +%% ------------------------------------------------------------------- + +-spec 'put!'(PathPattern, Data) -> NodePropsMap when + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + NodePropsMap :: node_props_map(). +%% @doc Creates or modifies a specific tree node in the tree structure. +%% +%% Calling this function is the same as calling {@link put/2} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see put/2. + +'put!'(PathPattern, Data) -> + Ret = put(PathPattern, Data), + unwrap_result(Ret). + +-spec 'put!'(StoreId, PathPattern, Data) -> NodePropsMap when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + NodePropsMap :: node_props_map(). +%% @doc Creates or modifies a specific tree node in the tree structure. +%% +%% Calling this function is the same as calling {@link put/3} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see put/3. + +'put!'(StoreId, PathPattern, Data) -> + Ret = put(StoreId, PathPattern, Data), + unwrap_result(Ret). + +-spec 'put!'(StoreId, PathPattern, Data, Extra | Options) -> Result when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + Extra :: #{keep_while => khepri_condition:keep_while()}, + Options :: command_options(), + Result :: NodePropsMap | NoRetIfAsync, + NodePropsMap :: node_props_map(), + NoRetIfAsync :: ok. +%% @doc Creates or modifies a specific tree node in the tree structure. +%% +%% Calling this function is the same as calling {@link put/4} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see put/4. + +'put!'(StoreId, PathPattern, Data, ExtraOrOptions) -> + Ret = put(StoreId, PathPattern, Data, ExtraOrOptions), + unwrap_result(Ret). + +-spec 'put!'(StoreId, PathPattern, Data, Extra, Options) -> Result when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + Extra :: #{keep_while => khepri_condition:keep_while()}, + Options :: command_options(), + Result :: NodePropsMap | NoRetIfAsync, + NodePropsMap :: node_props_map(), + NoRetIfAsync :: ok. +%% @doc Creates or modifies a specific tree node in the tree structure. +%% +%% Calling this function is the same as calling {@link put/5} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see put/5. + +'put!'(StoreId, PathPattern, Data, Extra, Options) -> + Ret = put(StoreId, PathPattern, Data, Extra, Options), + unwrap_result(Ret). + +-spec 'create!'(PathPattern, Data) -> NodePropsMap when + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + NodePropsMap :: node_props_map(). +%% @doc Creates a specific tree node in the tree structure only if it does not +%% exist. +%% +%% Calling this function is the same as calling {@link create/2} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see create/2. + +'create!'(PathPattern, Data) -> + Ret = create(PathPattern, Data), + unwrap_result(Ret). + +-spec 'create!'(StoreId, PathPattern, Data) -> NodePropsMap when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + NodePropsMap :: node_props_map(). +%% @doc Creates a specific tree node in the tree structure only if it does not +%% exist. +%% +%% Calling this function is the same as calling {@link create/3} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see create/3. + +'create!'(StoreId, PathPattern, Data) -> + Ret = create(StoreId, PathPattern, Data), + unwrap_result(Ret). + +-spec 'create!'(StoreId, PathPattern, Data, Extra | Options) -> Result when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + Extra :: #{keep_while => khepri_condition:keep_while()}, + Options :: command_options(), + Result :: NodePropsMap | NoRetIfAsync, + NodePropsMap :: node_props_map(), + NoRetIfAsync :: ok. +%% @doc Creates a specific tree node in the tree structure only if it does not +%% exist. +%% +%% Calling this function is the same as calling {@link create/4} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see create/4. + +'create!'(StoreId, PathPattern, Data, ExtraOrOptions) -> + Ret = create(StoreId, PathPattern, Data, ExtraOrOptions), + unwrap_result(Ret). + +-spec 'create!'(StoreId, PathPattern, Data, Extra, Options) -> Result when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + Extra :: #{keep_while => khepri_condition:keep_while()}, + Options :: command_options(), + Result :: NodePropsMap | NoRetIfAsync, + NodePropsMap :: node_props_map(), + NoRetIfAsync :: ok. +%% @doc Creates a specific tree node in the tree structure only if it does not +%% exist. +%% +%% Calling this function is the same as calling {@link create/5} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see create/5. + +'create!'(StoreId, PathPattern, Data, Extra, Options) -> + Ret = create(StoreId, PathPattern, Data, Extra, Options), + unwrap_result(Ret). + +-spec 'update!'(PathPattern, Data) -> NodePropsMap when + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + NodePropsMap :: node_props_map(). +%% @doc Updates a specific tree node in the tree structure only if it already +%% exists. +%% +%% Calling this function is the same as calling {@link update/2} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see update/2. + +'update!'(PathPattern, Data) -> + Ret = update(PathPattern, Data), + unwrap_result(Ret). + +-spec 'update!'(StoreId, PathPattern, Data) -> NodePropsMap when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + NodePropsMap :: node_props_map(). +%% @doc Updates a specific tree node in the tree structure only if it already +%% exists. +%% +%% Calling this function is the same as calling {@link update/3} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see update/3. + +'update!'(StoreId, PathPattern, Data) -> + Ret = update(StoreId, PathPattern, Data), + unwrap_result(Ret). + +-spec 'update!'(StoreId, PathPattern, Data, Extra | Options) -> Result when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + Extra :: #{keep_while => khepri_condition:keep_while()}, + Options :: command_options(), + Result :: NodePropsMap | NoRetIfAsync, + NodePropsMap :: node_props_map(), + NoRetIfAsync :: ok. +%% @doc Updates a specific tree node in the tree structure only if it already +%% exists. +%% +%% Calling this function is the same as calling {@link update/4} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see update/4. + +'update!'(StoreId, PathPattern, Data, ExtraOrOptions) -> + Ret = update(StoreId, PathPattern, Data, ExtraOrOptions), + unwrap_result(Ret). + +-spec 'update!'(StoreId, PathPattern, Data, Extra, Options) -> Result when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Data :: khepri_payload:payload() | data() | fun(), + Extra :: #{keep_while => khepri_condition:keep_while()}, + Options :: command_options(), + Result :: NodePropsMap | NoRetIfAsync, + NodePropsMap :: node_props_map(), + NoRetIfAsync :: ok. +%% @doc Updates a specific tree node in the tree structure only if it already +%% exists. +%% +%% Calling this function is the same as calling {@link update/5} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. If +%% there is an error, an exception is thrown. +%% +%% @see update/5. + +'update!'(StoreId, PathPattern, Data, Extra, Options) -> + Ret = update(StoreId, PathPattern, Data, Extra, Options), + unwrap_result(Ret). + +-spec 'compare_and_swap!'(PathPattern, DataPattern, Data) -> NodePropsMap when + PathPattern :: khepri_path:pattern(), + DataPattern :: ets:match_pattern(), + Data :: khepri_payload:payload() | data() | fun(), + NodePropsMap :: node_props_map(). +%% @doc Updates a specific tree node in the tree structure only if it already +%% exists and its data matches the given `DataPattern'. +%% +%% Calling this function is the same as calling {@link compare_and_swap/3} but +%% the result is unwrapped (from the `{ok, Result}' tuple) and returned +%% directly. If there is an error, an exception is thrown. +%% +%% @see compare_and_swap/3. + +'compare_and_swap!'(PathPattern, DataPattern, Data) -> + Ret = compare_and_swap(PathPattern, DataPattern, Data), + unwrap_result(Ret). + +-spec 'compare_and_swap!'( + StoreId, PathPattern, DataPattern, Data) -> + NodePropsMap when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + DataPattern :: ets:match_pattern(), + Data :: khepri_payload:payload() | data() | fun(), + NodePropsMap :: node_props_map(). +%% @doc Updates a specific tree node in the tree structure only if it already +%% exists and its data matches the given `DataPattern'. +%% +%% Calling this function is the same as calling {@link compare_and_swap/4} but +%% the result is unwrapped (from the `{ok, Result}' tuple) and returned +%% directly. If there is an error, an exception is thrown. +%% +%% @see compare_and_swap/4. + +'compare_and_swap!'(StoreId, PathPattern, DataPattern, Data) -> + Ret = compare_and_swap(StoreId, PathPattern, DataPattern, Data), + unwrap_result(Ret). + +-spec 'compare_and_swap!'( + StoreId, PathPattern, DataPattern, Data, Extra | Options) -> + Result when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + DataPattern :: ets:match_pattern(), + Data :: khepri_payload:payload() | data() | fun(), + Extra :: #{keep_while => khepri_condition:keep_while()}, + Options :: command_options(), + Result :: NodePropsMap | NoRetIfAsync, + NodePropsMap :: node_props_map(), + NoRetIfAsync :: ok. +%% @doc Updates a specific tree node in the tree structure only if it already +%% exists and its data matches the given `DataPattern'. +%% +%% Calling this function is the same as calling {@link compare_and_swap/5} but +%% the result is unwrapped (from the `{ok, Result}' tuple) and returned +%% directly. If there is an error, an exception is thrown. +%% +%% @see compare_and_swap/5. + +'compare_and_swap!'( + StoreId, PathPattern, DataPattern, Data, ExtraOrOptions) -> + Ret = compare_and_swap( + StoreId, PathPattern, DataPattern, Data, ExtraOrOptions), + unwrap_result(Ret). + +-spec 'compare_and_swap!'( + StoreId, PathPattern, DataPattern, Data, Extra, Options) -> + Result when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + DataPattern :: ets:match_pattern(), + Data :: khepri_payload:payload() | data() | fun(), + Extra :: #{keep_while => khepri_condition:keep_while()}, + Options :: command_options(), + Result :: NodePropsMap | NoRetIfAsync, + NodePropsMap :: node_props_map(), + NoRetIfAsync :: ok. +%% @doc Updates a specific tree node in the tree structure only if it already +%% exists and its data matches the given `DataPattern'. +%% +%% Calling this function is the same as calling {@link compare_and_swap/6} but +%% the result is unwrapped (from the `{ok, Result}' tuple) and returned +%% directly. If there is an error, an exception is thrown. +%% +%% @see compare_and_swap/6. + +'compare_and_swap!'( + StoreId, PathPattern, DataPattern, Data, Extra, Options) -> + Ret = compare_and_swap( + StoreId, PathPattern, DataPattern, Data, Extra, Options), + unwrap_result(Ret). + +-spec 'get!'(PathPattern) -> NodePropsMap when + PathPattern :: khepri_path:pattern(), + NodePropsMap :: node_props_map(). +%% @doc Returns all tree nodes matching the path pattern. +%% +%% Calling this function is the same as calling {@link get/1} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. It +%% closer to Elixir conventions in pipelines however. +%% +%% @see get/1. + +'get!'(PathPattern) -> + Ret = get(PathPattern), + unwrap_result(Ret). + +-spec 'get!' +(StoreId, PathPattern) -> NodePropsMap when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + NodePropsMap :: node_props_map(); +(PathPattern, Options) -> NodePropsMap when + PathPattern :: khepri_path:pattern(), + Options :: query_options(), + NodePropsMap :: node_props_map(). +%% @doc Returns all tree nodes matching the path pattern. +%% +%% Calling this function is the same as calling {@link get/2} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. It +%% closer to Elixir conventions in pipelines however. +%% +%% @see get/2. + +'get!'(StoreIdOrPathPattern, PathPatternOrOptions) -> + Ret = get(StoreIdOrPathPattern, PathPatternOrOptions), + unwrap_result(Ret). + +-spec 'get!'(StoreId, PathPattern, Options) -> NodePropsMap when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Options :: query_options(), + NodePropsMap :: node_props_map(). +%% @doc Returns all tree nodes matching the path pattern. +%% +%% Calling this function is the same as calling {@link get/3} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. It +%% closer to Elixir conventions in pipelines however. +%% +%% @see get/3. + +'get!'(StoreId, PathPattern, Options) -> + Ret = get(StoreId, PathPattern, Options), + unwrap_result(Ret). + +-spec 'delete!'(PathPattern) -> NodePropsMap when + PathPattern :: khepri_path:pattern(), + NodePropsMap :: node_props_map(). +%% @doc Deletes all tree nodes matching the path pattern. +%% +%% Calling this function is the same as calling {@link delete/1} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. It +%% closer to Elixir conventions in pipelines however. +%% +%% @see delete/1. + +'delete!'(PathPattern) -> + Ret = delete(PathPattern), + unwrap_result(Ret). + +-spec 'delete!' +(StoreId, PathPattern) -> NodePropsMap when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + NodePropsMap :: node_props_map(); +(PathPattern, Options) -> NodePropsMap when + PathPattern :: khepri_path:pattern(), + Options :: query_options(), + NodePropsMap :: node_props_map(). +%% @doc Deletes all tree nodes matching the path pattern. +%% +%% Calling this function is the same as calling {@link delete/2} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. It +%% closer to Elixir conventions in pipelines however. +%% +%% @see delete/2. + +'delete!'(StoreIdOrPathPattern, PathPatternOrOptions) -> + Ret = delete(StoreIdOrPathPattern, PathPatternOrOptions), + unwrap_result(Ret). + +-spec 'delete!'(StoreId, PathPattern, Options) -> NodePropsMap when + StoreId :: store_id(), + PathPattern :: khepri_path:pattern(), + Options :: query_options(), + NodePropsMap :: node_props_map(). +%% @doc Deletes all tree nodes matching the path pattern. +%% +%% Calling this function is the same as calling {@link delete/3} but the result +%% is unwrapped (from the `{ok, Result}' tuple) and returned directly. It +%% closer to Elixir conventions in pipelines however. +%% +%% @see delete/3. + +'delete!'(StoreId, PathPattern, Options) -> + Ret = delete(StoreId, PathPattern, Options), + unwrap_result(Ret). + +-spec unwrap_result(Ret) -> NodePropsMap when + Ret :: result() | ok, + NodePropsMap :: node_props_map() | ok. +%% @private + +unwrap_result({ok, Result}) -> Result; +unwrap_result(ok) -> ok; +unwrap_result({error, Reason}) -> error(Reason). + %% ------------------------------------------------------------------- %% Public helpers. %% ------------------------------------------------------------------- diff --git a/src/khepri_path.erl b/src/khepri_path.erl index 8e916caa..b89d5934 100644 --- a/src/khepri_path.erl +++ b/src/khepri_path.erl @@ -52,6 +52,8 @@ from_binary/1, to_string/1, to_binary/1, + sigil_p/2, + sigil_P/2, combine_with_conditions/2, targets_specific_node/1, component_targets_specific_node/1, @@ -240,6 +242,50 @@ from_string(NotPath) -> from_binary(MaybeString) -> from_string(MaybeString). +-spec sigil_p(PathPattern, Options) -> NativePathPattern when + PathPattern :: pattern(), + Options :: [char()], + NativePathPattern :: native_pattern(). +%% @doc Elixir sigil to parse Unix-like path using the `~p"/:path/:to/node"' +%% syntax. +%% +%% The lowercase `~p' sigil means that the string will go through +%% interpolation first before this function is called. +%% +%% @see sigil_P/2. +%% +%% @private + +sigil_p(PathPattern, _Options) -> + try + from_string(PathPattern) + catch + throw:Reason:Stacktrace -> + erlang:raise(error, Reason, Stacktrace) + end. + +-spec sigil_P(PathPattern, Options) -> NativePathPattern when + PathPattern :: pattern(), + Options :: [char()], + NativePathPattern :: native_pattern(). +%% @doc Elixir sigil to parse Unix-like path using the `~P"/:path/:to/node"' +%% syntax. +%% +%% The uppercase `~P' sigil means that the string will NOT go through +%% interpolation first before this function is called. +%% +%% @see sigil_p/2. +%% +%% @private + +sigil_P(PathPattern, _Options) -> + try + from_string(PathPattern) + catch + throw:Reason:Stacktrace -> + erlang:raise(error, Reason, Stacktrace) + end. + from_string([Component | _] = Rest, ReversedPath) when ?IS_NODE_ID(Component) orelse ?IS_CONDITION(Component) -> diff --git a/test/bang_functions.erl b/test/bang_functions.erl new file mode 100644 index 00000000..0ee348a5 --- /dev/null +++ b/test/bang_functions.erl @@ -0,0 +1,187 @@ +%% 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 © 2022 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(bang_functions). + +-include_lib("eunit/include/eunit.hrl"). + +-include("include/khepri.hrl"). +-include("src/internal.hrl"). +-include("test/helpers.hrl"). + +get_test_() -> + {setup, + fun() -> test_ra_server_helpers:setup(?FUNCTION_NAME) end, + fun(Priv) -> test_ra_server_helpers:cleanup(Priv) end, + [?_assertEqual( + {ok, #{[foo] => #{}}}, + khepri:create(?FUNCTION_NAME, [foo], foo_value)), + + %% Get existing node. + ?_assertEqual( + khepri:get(?FUNCTION_NAME, [foo]), + {ok, khepri:'get!'(?FUNCTION_NAME, [foo])}), + ?_assertEqual( + khepri:get(?FUNCTION_NAME, [foo], #{}), + {ok, khepri:'get!'(?FUNCTION_NAME, [foo], #{})}), + + %% Get non-existing node. + ?_assertEqual( + khepri:get(?FUNCTION_NAME, [bar]), + {ok, khepri:'get!'(?FUNCTION_NAME, [bar])}), + ?_assertEqual( + khepri:get(?FUNCTION_NAME, [bar], #{}), + {ok, khepri:'get!'(?FUNCTION_NAME, [bar], #{})}), + ?_assertError(noproc, khepri:'get!'([foo])) + ]}. + +put_test_() -> + {setup, + fun() -> test_ra_server_helpers:setup(?FUNCTION_NAME) end, + fun(Priv) -> test_ra_server_helpers:cleanup(Priv) end, + [?_assertEqual( + #{[foo] => #{}}, + khepri:'put!'(?FUNCTION_NAME, [foo], value1)), + ?_assertEqual( + #{[foo] => #{data => value1, + payload_version => 1, + child_list_version => 1, + child_list_length => 0}}, + khepri:'put!'(?FUNCTION_NAME, [foo], value2, #{})), + ?_assertEqual( + #{[foo] => #{data => value2, + payload_version => 2, + child_list_version => 1, + child_list_length => 0}}, + khepri:'put!'(?FUNCTION_NAME, [foo], value3, #{}, #{})), + ?_assertEqual( + ok, + khepri:'put!'(?FUNCTION_NAME, [foo], value4, #{async => true})), + ?_assertError(noproc, khepri:'put!'([foo], value)) + ]}. + +create_test_() -> + {setup, + fun() -> test_ra_server_helpers:setup(?FUNCTION_NAME) end, + fun(Priv) -> test_ra_server_helpers:cleanup(Priv) end, + [?_assertEqual( + #{[foo] => #{}}, + khepri:'create!'(?FUNCTION_NAME, [foo], value1)), + ?_assertError( + {mismatching_node, + #{condition := #if_node_exists{exists = false}, + node_name := foo, + node_path := [foo], + node_is_target := true, + node_props := #{data := value1, + payload_version := 1, + child_list_version := 1, + child_list_length := 0}}}, + khepri:'create!'(?FUNCTION_NAME, [foo], value2, #{})), + ?_assertError( + {mismatching_node, + #{condition := #if_node_exists{exists = false}, + node_name := foo, + node_path := [foo], + node_is_target := true, + node_props := #{data := value1, + payload_version := 1, + child_list_version := 1, + child_list_length := 0}}}, + khepri:'create!'(?FUNCTION_NAME, [foo], value3, #{}, #{})), + ?_assertError(noproc, khepri:'create!'([foo], value)) + ]}. + +update_test_() -> + {setup, + fun() -> test_ra_server_helpers:setup(?FUNCTION_NAME) end, + fun(Priv) -> test_ra_server_helpers:cleanup(Priv) end, + [?_assertError( + {node_not_found, + #{condition := #if_all{conditions = + [foo, + #if_node_exists{exists = true}]}, + node_name := foo, + node_path := [foo], + node_is_target := true}}, + khepri:'update!'(?FUNCTION_NAME, [foo], value1)), + ?_assertEqual( + #{[foo] => #{}}, + khepri:'create!'(?FUNCTION_NAME, [foo], value1)), + ?_assertEqual( + #{[foo] => #{data => value1, + payload_version => 1, + child_list_version => 1, + child_list_length => 0}}, + khepri:'update!'(?FUNCTION_NAME, [foo], value2, #{})), + ?_assertEqual( + #{[foo] => #{data => value2, + payload_version => 2, + child_list_version => 1, + child_list_length => 0}}, + khepri:'update!'(?FUNCTION_NAME, [foo], value3, #{}, #{})), + ?_assertError(noproc, khepri:'update!'([foo], value)) + ]}. + +compare_and_swap_test_() -> + {setup, + fun() -> test_ra_server_helpers:setup(?FUNCTION_NAME) end, + fun(Priv) -> test_ra_server_helpers:cleanup(Priv) end, + [?_assertError( + {node_not_found, + #{condition := #if_all{conditions = + [foo, + #if_data_matches{pattern = value0}]}, + node_name := foo, + node_path := [foo], + node_is_target := true}}, + khepri:'compare_and_swap!'(?FUNCTION_NAME, [foo], value0, value1)), + ?_assertEqual( + #{[foo] => #{}}, + khepri:'create!'(?FUNCTION_NAME, [foo], value1)), + ?_assertEqual( + #{[foo] => #{data => value1, + payload_version => 1, + child_list_version => 1, + child_list_length => 0}}, + khepri:'compare_and_swap!'( + ?FUNCTION_NAME, [foo], value1, value2, #{})), + ?_assertError( + {mismatching_node, + #{condition := #if_data_matches{pattern = value1}, + node_name := foo, + node_path := [foo], + node_is_target := true, + node_props := #{data := value2, + payload_version := 2, + child_list_version := 1, + child_list_length := 0}}}, + khepri:'compare_and_swap!'( + ?FUNCTION_NAME, [foo], value1, value3, #{}, #{})), + ?_assertError( + noproc, + khepri:'compare_and_swap!'([foo], old_value, new_value)) + ]}. + +delete_test_() -> + {setup, + fun() -> test_ra_server_helpers:setup(?FUNCTION_NAME) end, + fun(Priv) -> test_ra_server_helpers:cleanup(Priv) end, + [?_assertEqual( + {ok, #{[foo] => #{}}}, + khepri:create(?FUNCTION_NAME, [foo], value1)), + ?_assertEqual( + #{[foo] => #{data => value1, + payload_version => 1, + child_list_version => 1, + child_list_length => 0}}, + khepri:'delete!'(?FUNCTION_NAME, [foo])), + ?_assertEqual( + #{}, + khepri:'delete!'(?FUNCTION_NAME, [foo], #{})), + ?_assertError(noproc, khepri:'delete!'([foo])) + ]}. diff --git a/test/sigils.erl b/test/sigils.erl new file mode 100644 index 00000000..d2731b76 --- /dev/null +++ b/test/sigils.erl @@ -0,0 +1,38 @@ +%% 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 © 2022 VMware, Inc. or its affiliates. All rights reserved. +%% + +-module(sigils). + +-include_lib("eunit/include/eunit.hrl"). + +-dialyzer({nowarn_function, [ + %% The following functions explicitely break the + %% spec contract on purpose to generate an + %% exception. + sigil_p_error_test/0, + sigil_P_error_test/0 + ]}). + +sigil_p_test() -> + ?assertEqual( + [foo], + khepri_path:sigil_p("/:foo", [])). + +sigil_p_error_test() -> + ?assertError( + {invalid_path, #{path := not_a_path}}, + khepri_path:sigil_p(not_a_path, [])). + +sigil_P_test() -> + ?assertEqual( + [foo], + khepri_path:sigil_P("/:foo", [])). + +sigil_P_error_test() -> + ?assertError( + {invalid_path, #{path := not_a_path}}, + khepri_path:sigil_P(not_a_path, [])).