Skip to content

Adds Proto.Transfromations #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions lib/util/helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
defmodule Util.Helpers do
@moduledoc """
Miscellaneous helpers
"""

def non_empty_value_or_default(map, key, default) do
case Map.get(map, key) do
val when is_integer(val) and val > 0 -> {:ok, val}
val when is_binary(val) and val != "" -> {:ok, val}
val when is_list(val) and length(val) > 0 -> {:ok, val}
_ -> {:ok, default}
end
end

def not_empty_string(map, key, error_atom \\ "") do
case Map.get(map, key) do
value when is_binary(value) and value != "" ->
{:ok, value}
error_val ->
"'#{key}' - invalid value: '#{error_val}', it must be a not empty string."
|> Util.ToTuple.error(error_atom)
end
end
end
15 changes: 15 additions & 0 deletions lib/util/log_tee.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Util.LogTee do
require Logger

defmacro tee_(item, tag, severity) do
quote do
Logger.unquote(severity)( "#{unquote(tag)}: #{inspect unquote(item)}")
unquote(item)
end
end

def debug(item, tag) when is_binary(tag), do: tee_(item, tag, :debug)
def info(item, tag) when is_binary(tag), do: tee_(item, tag, :info)
def warn(item, tag) when is_binary(tag), do: tee_(item, tag, :warn)
def error(item, tag) when is_binary(tag), do: tee_(item, tag, :error)
end
56 changes: 56 additions & 0 deletions lib/util/proto/transformations.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
defmodule Util.Proto.Transformations do
@moduledoc """
This module is intended as a simple way for initializing deeply nested Protobuf
structures defined by protobuf-elixir modules.
"""

@doc """
## Examples:

iex> alias Util.Proto.Transformations
iex> Transformations.string_to_enum_atom_or_0("name", "")
0

iex> alias Util.Proto.Transformations
iex> Transformations.string_to_enum_atom_or_0("name", 1)
0

iex> alias Util.Proto.Transformations
iex> Transformations.string_to_enum_atom_or_0("name", "value")
:Value
"""
def string_to_enum_atom_or_0(_field_name, field_value)
when is_binary(field_value) and field_value != "" do
field_value |> String.upcase() |> String.to_atom()
end
def string_to_enum_atom_or_0(_field_name, _field_value), do: 0


@doc """
## Examples:

iex> alias Util.Proto.Transformations
iex> Transformations.date_time_to_timestamps("name", nil)
%{seconds: 0, nanos: 0}

iex> alias Util.Proto.Transformations
iex> {:ok, time} = DateTime.new(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
iex> Transformations.date_time_to_timestamps("name", time)
%{seconds: 1464096368, nanos: 3000000}

iex> alias Util.Proto.Transformations
iex> Transformations.date_time_to_timestamps("name", %{seconds: 10, nanos: 10})
%{seconds: 10, nanos: 10}

"""
def date_time_to_timestamps(_field_name, nil), do: %{seconds: 0, nanos: 0}
def date_time_to_timestamps(_field_name, date_time = %DateTime{}) do
%{}
|> Map.put(:seconds, DateTime.to_unix(date_time, :second))
|> Map.put(:nanos, elem(date_time.microsecond, 0) * 1_000)
end
def date_time_to_timestamps(_field_name, value), do: value

def atom_to_lower_string(_field_name, value),
do: value |> Atom.to_string() |> String.downcase()
end
183 changes: 183 additions & 0 deletions lib/util/validator.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
defmodule Util.Validator do
alias Util.ToTuple
import ToTuple

@type global_validator_name :: atom()
@type validator_argument :: list()

@type validator ::
global_validator_name
| {global_validator_name, validator_argument()}
| (value_to_validate :: any() -> validator_result())
| (value_to_validate :: any(), validator_argument() -> validator_result())

@type validator_callback :: (value :: any() -> validator_result())
@type validator_result() :: ToTuple.ok_tuple() | ToTuple.error_tuple(String.t())

@doc """
Validates the value with a list of validators.

## Examples

iex> validate(1, [eq: 1])
{:ok, 1}
iex> validate(1, fn _ -> {:error, "I've failed"} end)
{:error, "I've failed"}

iex> validate(1, [fn _ -> {:error, "I've failed"} end, fn _ -> {:error, "I've failed too"} end, fn _ -> {:error, "So did I"} end])
{:error, "I've failed, I've failed too, So did I"}

iex> validate(1, [fn _ -> {:error, "I've failed"} end, fn _ -> {:error, "I've failed too"} end, fn _ -> {:error, "So did I"} end])
{:error, "I've failed, I've failed too, So did I"}

"""
@spec validate(any, [validator()]) :: validator_result()
def validate(value_to_validate, validators) do
validators
|> compile()
|> Enum.map(fn validate ->
validate.(value_to_validate)
end)
|> resolve(value_to_validate)
end

@doc """
Compiles validators to a list of functions accepting value to validate as an argument.

## Examples

iex> validators = [:identity, {:eq, 2}, fn _ -> {:ok, "I'm fine"} end, {fn _, name -> {:error, "\#{name} - I'm not fine"} end, "Joe"}]
...> compiled_validators = compile(validators)
...> Enum.map(compiled_validators, & &1.(2))
[2, 2, {:ok, "I'm fine"}, {:error, "Joe - I'm not fine"}]
"""
@spec compile(validators :: [validator]) :: [validator_callback]
def compile(validators) do
validators
|> to_list
|> ensure_validator_arguments()
|> Enum.map(&normalize/1)
end

@doc """
Resolves the validation result.
If every validation resolves to ok tuple, wrapped value will be returned.
In case any validations resolves to error tuple, error messages from validations will be
concatenated and returned as error message.
"""
@spec resolve(results :: [validator_result()], value :: term()) ::
ToTuple.ok_tuple(value :: term()) | ToTuple.error_tuple(String.t())
def resolve(results, value) do
results
|> Enum.filter(fn
{:error, _} -> true
_ -> false
end)
|> case do
errors when errors == [] ->
wrap(value)

errors ->
errors
|> Enum.map_join(", ", &elem(&1, 1))
|> error()
end
|> case do
{:ok, _} -> wrap(value)
error -> error
end
end

@doc """
This function makes sure that all validators have an argument.
When argument is a list - this function is recursively called on each element.
If not `[]` is used as a default

## Examples
iex> ensure_validator_arguments({:a, []})
{:a, []}

iex> ensure_validator_arguments(:a)
{:a, []}

iex> ensure_validator_arguments([:a, :b, :c])
[{:a, []}, {:b, []}, {:c, []}]

iex> ensure_validator_arguments([:a, :b, c: 2, d: 3])
[{:a, []}, {:b, []}, {:c, 2}, {:d, 3}]

iex> ensure_validator_arguments([:a, [:b, [:c]]])
[{:a, []}, [{:b, []}, [{:c, []}]]]

"""
def ensure_validator_arguments(validator) do
case validator do
{validator, validator_opts} ->
{validator, validator_opts}
validators when is_list(validators) ->
validators
|> Enum.map(&ensure_validator_arguments/1)

validator ->
{validator, []}
end
end

@doc """
Normalizes a validator tuple to a function that accepts one value - the value to validate.
Validations can run only on ok_tuples and plain values. Error tuples as values **will not** trigger validations.

## Examples
iex> is_function(normalize({:some_global_validator, []}), 1)
true

iex> is_function(normalize({& &1, []}), 1)
true

iex> is_function(normalize({& &1 + &2, []}), 1)
true

iex> normalize(:a)
** (RuntimeError) only atoms, one-argument and two-argument functions callbacks can be validators, got :a
"""
def normalize(validator_tuple) do
validator_tuple
|> case do
{validator_name, validator_argument} when is_atom(validator_name) ->
fn value ->
value
|> unwrap(fn value ->
Util.Validator.BaseValidator.select(validator_name).(
value,
validator_argument
)
end)
end

{validator_func, validator_argument} when is_function(validator_func, 2) ->
fn value ->
value
|> unwrap(fn value ->
validator_func.(value, validator_argument)
end)
end

{validator_func, _validator_argument} when is_function(validator_func, 1) ->
validator_func

invalid ->
raise(
"only atoms, one-argument and two-argument functions callbacks can be validators, got #{inspect(invalid)}"
)
end
end

defp to_list(value) do
value
|> is_list()
|> case do
true -> value
_ -> [value]
end
end
end
Loading