Packages
FerricFlow durable workflows and queues with native-protocol storage, Raft durability, and Bitcask persistence.
Current section
Files
Jump to
Current section
Files
lib/ferricstore/embedded_string_validation.ex
defmodule Ferricstore.EmbeddedStringValidation do
@moduledoc false
alias Ferricstore.CommandTime
alias Ferricstore.Commands.Strings.SetOptions
@set_options_default %{
expire_at_ms: 0,
nx: false,
xx: false,
get: false,
keepttl: false,
has_expiry: false
}
@spec parse_set_options(keyword()) :: {:ok, map()} | {:error, binary()}
def parse_set_options(opts) when is_list(opts) do
with {:ok, ast_opts} <- set_option_ast(opts) do
SetOptions.from_ast(ast_opts, @set_options_default)
end
end
def parse_set_options(_opts), do: {:error, "ERR syntax error"}
@spec parse_getex_options(keyword()) ::
{:ok, non_neg_integer() | nil} | {:error, binary()}
def parse_getex_options(opts) when is_list(opts) do
opts
|> Enum.reduce_while({:ok, %{persist: :unset, ttl: :unset}}, fn
{:ttl, ttl}, {:ok, %{ttl: :unset} = parsed} when is_integer(ttl) and ttl > 0 ->
{:cont, {:ok, %{parsed | ttl: ttl}}}
{:ttl, ttl}, {:ok, %{ttl: :unset}} when not is_integer(ttl) or ttl <= 0 ->
{:halt, {:error, "ERR invalid expire time in 'getex' command"}}
{:persist, persist}, {:ok, %{persist: :unset} = parsed} when is_boolean(persist) ->
{:cont, {:ok, %{parsed | persist: persist}}}
_invalid_or_duplicate, _acc ->
{:halt, {:error, "ERR syntax error"}}
end)
|> normalize_getex_options()
end
def parse_getex_options(_opts), do: {:error, "ERR syntax error"}
@spec relative_expiry(term(), pos_integer(), binary()) ::
{:ok, non_neg_integer()} | {:error, binary()}
def relative_expiry(value, multiplier, command)
when is_integer(multiplier) and multiplier > 0 and is_binary(command) do
with :ok <- validate_positive_expiry(value, command) do
{:ok, CommandTime.now_ms() + value * multiplier}
end
end
@spec validate_positive_expiry(term(), binary()) :: :ok | {:error, binary()}
def validate_positive_expiry(value, _command) when is_integer(value) and value > 0, do: :ok
def validate_positive_expiry(_value, command) when is_binary(command),
do: {:error, "ERR invalid expire time in '#{command}' command"}
@spec validate_value_size(map(), term()) :: :ok | {:error, binary()}
def validate_value_size(ctx, value) when is_binary(value) do
max_value_size = Map.get(ctx, :max_value_size, 1_048_576)
if byte_size(value) > max_value_size do
{:error, "ERR value too large (#{byte_size(value)} bytes, max #{max_value_size} bytes)"}
else
:ok
end
end
def validate_value_size(_ctx, _value), do: :ok
defp set_option_ast(opts) do
opts
|> Enum.reduce_while({:ok, []}, fn
{:ttl, 0}, {:ok, acc} -> {:cont, {:ok, acc}}
{:ttl, value}, {:ok, acc} -> {:cont, {:ok, [{:px, value} | acc]}}
{:exat, value}, {:ok, acc} -> {:cont, {:ok, [{:exat, value} | acc]}}
{:pxat, value}, {:ok, acc} -> {:cont, {:ok, [{:pxat, value} | acc]}}
{:nx, true}, {:ok, acc} -> {:cont, {:ok, [:nx | acc]}}
{:nx, false}, {:ok, acc} -> {:cont, {:ok, acc}}
{:xx, true}, {:ok, acc} -> {:cont, {:ok, [:xx | acc]}}
{:xx, false}, {:ok, acc} -> {:cont, {:ok, acc}}
{:get, true}, {:ok, acc} -> {:cont, {:ok, [:get | acc]}}
{:get, false}, {:ok, acc} -> {:cont, {:ok, acc}}
{:keepttl, true}, {:ok, acc} -> {:cont, {:ok, [:keepttl | acc]}}
{:keepttl, false}, {:ok, acc} -> {:cont, {:ok, acc}}
{:cache, value}, {:ok, acc} when is_atom(value) -> {:cont, {:ok, acc}}
_invalid, _acc -> {:halt, {:error, "ERR syntax error"}}
end)
|> case do
{:ok, ast_opts} -> {:ok, Enum.reverse(ast_opts)}
error -> error
end
end
defp normalize_getex_options({:ok, %{persist: true, ttl: ttl}}) when is_integer(ttl),
do: {:error, "ERR syntax error"}
defp normalize_getex_options({:ok, %{persist: true}}), do: {:ok, 0}
defp normalize_getex_options({:ok, %{ttl: ttl}}) when is_integer(ttl),
do: {:ok, CommandTime.now_ms() + ttl}
defp normalize_getex_options({:ok, _parsed}), do: {:ok, nil}
defp normalize_getex_options(error), do: error
end