Skip to content

Commit

Permalink
Separate GuardedStruct from Mishka developer tools
Browse files Browse the repository at this point in the history
  • Loading branch information
shahryarjb committed Nov 1, 2024
1 parent 2664ca4 commit d986b6d
Show file tree
Hide file tree
Showing 21 changed files with 10,490 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
38 changes: 38 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
guarded_struct-*.tar

# Temporary files, for example, from tests.
/tmp/


# General
.DS_Store
Desktop.ini
.AppleDouble
.LSOverride
.tool-versions
name

# custom
/lib/example
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# GuardedStruct

**TODO: Add description**

## Installation

If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `guarded_struct` to your list of dependencies in `mix.exs`:

```elixir
def deps do
[
{:guarded_struct, "~> 0.1.0"}
]
end
```

Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/guarded_struct>.

198 changes: 198 additions & 0 deletions lib/derive/derive.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
defmodule GuardedStruct.Derive do
alias GuardedStruct.Derive.{Parser, SanitizerDerive, ValidationDerive}

@spec derive(
{:error, any(), any()}
| {:ok, any(), list(String.t() | map())}
| {:error, any(), :halt}
| {:error, :nested, list(), any(), [binary()]}
) :: {:ok, map()} | {:error, any()}
def derive({:error, type, message, :halt}) do
{:error, type, message}
end

def derive({:error, :nested, builders_errors, data, derive_inputs}),
do: derive({:ok, data, derive_inputs}, builders_errors)

def derive({:error, _, _} = error), do: error

def derive({:error, _} = error), do: error

@spec derive({:ok, any(), list(String.t() | map())}, list()) ::
{:ok, map()} | {:error, list()}
def derive({:ok, data, derive_inputs}, extra_error \\ []) do
reduced_fields =
Enum.reduce(derive_inputs, %{}, fn map, acc ->
derives = Parser.parser(map.derive)
field = Map.get(data, map.field)
hint = Map.get(map, :hint) || []

update_reduced_fields(field, derives, hint, map, acc)
end)

{:error, get_error} = error = error_handler(reduced_fields, extra_error)

if length(get_error) == 0, do: {:ok, Map.merge(data, reduced_fields)}, else: error
end

defp update_reduced_fields(nil, _parsed_derive, _hint, _map, acc), do: acc

defp update_reduced_fields(get_field, parsed_derive, hints, map, acc)
when is_list(parsed_derive) and parsed_derive != [] do
# Temporary way to find it is list conditional or not
list_data? = is_list(get_field) and length(get_field) == length(parsed_derive)

get_field =
if list_data? do
get_field
else
stream = Stream.duplicate(get_field, length(parsed_derive))
Enum.to_list(stream)
end

converted_validated_values =
Enum.zip([parsed_derive, get_field, hints])
|> Enum.map(fn {derive, value, hint} ->
derive = if(derive == [], do: nil, else: derive)

{all_data, validated_errors} =
{map.field, value}
|> SanitizerDerive.call(Map.get(derive || %{}, :sanitize))
|> ValidationDerive.call(Map.get(derive || %{}, :validate), hint)

if length(validated_errors) > 0, do: {:error, validated_errors}, else: all_data
end)

{errors, data} = derive_list_values_and_errors_divider(converted_validated_values)

if list_data? do
Map.put(acc, map.field, if(length(errors) > 0, do: {:error, errors}, else: data))
else
Map.put(acc, map.field, if(length(data) > 0, do: List.first(data), else: {:error, errors}))
end
end

defp update_reduced_fields(get_field, parsed_derive, hint, map, acc) do
# destruct because we consider empty list default value when there is no derive
parsed_derive = if(parsed_derive == [], do: nil, else: parsed_derive)

{all_data, validated_errors} =
{map.field, get_field}
|> SanitizerDerive.call(Map.get(parsed_derive || %{}, :sanitize))
|> ValidationDerive.call(Map.get(parsed_derive || %{}, :validate), hint)

converted_validated_values =
if length(validated_errors) > 0, do: {:error, validated_errors}, else: all_data

Map.put(acc, map.field, converted_validated_values)
end

defp derive_list_values_and_errors_divider(data) do
{error, no_error} =
data
|> Enum.split_with(&(is_tuple(&1) and elem(&1, 0) == :error))

converted_error = Enum.map(error, fn {:error, errors} -> errors end) |> Enum.concat()

{converted_error, no_error}
end

@spec error_handler(map(), list(any())) :: {:error, any()}
def error_handler(reduced_fields, extra_error \\ []) do
errors =
Enum.find(extra_error, fn %{field: _, errors: errorMap} ->
!is_list(errorMap) and errorMap.action == :required_fields
end)
|> case do
nil ->
get_error =
reduced_fields
|> Map.values()
|> Enum.filter(&(is_tuple(&1) && elem(&1, 0) == :error))
|> Enum.map(fn {:error, errors} -> errors end)
|> Enum.concat()
|> halt_errors()

get_error ++ extra_error

_ ->
extra_error
end

{:error, errors}
end

defp halt_errors(errors_list) do
errors_list
|> Enum.reduce_while([], fn item, acc ->
if Map.get(item, :status) == :halt,
do: {:halt, acc ++ [Map.delete(item, :status)]},
else: {:cont, acc ++ [item]}
end)
end

@spec get_derives_from_success_conditional_data(list(any())) :: any()
@doc false
def get_derives_from_success_conditional_data(conds) do
Enum.reduce(conds, [], fn
{field, {{:ok, _data}, opts}}, acc ->
case Keyword.keyword?(opts) do
true ->
get_derive = Keyword.get(opts, :derive, [])
get_hint = Keyword.get(opts, :hint, [])
acc ++ [Map.new([{:derive, get_derive}, {:field, field}, {:hint, get_hint}])]

false when is_list(opts) ->
%{derive: derives, hint: hints} =
Enum.reduce(opts, %{derive: [], hint: []}, fn item, acc ->
get_derive = Keyword.get(item, :derive, [])
get_hint = Keyword.get(item, :hint, [])

Map.merge(acc, %{derive: acc.derive ++ [get_derive], hint: acc.hint ++ [get_hint]})
end)

acc ++ [Map.new([{:derive, derives}, {:field, field}, {:hint, hints}])]

_ ->
# We do not cover this setuation
acc
end

{field, values}, acc ->
%{derive: derives, hint: hints} =
Enum.reduce(values, %{derive: [], hint: []}, fn {{:ok, _value}, opts}, acc ->
get_derive = Keyword.get(opts, :derive, [])
get_hint = Keyword.get(opts, :hint, [])

Map.merge(acc, %{derive: acc.derive ++ [get_derive], hint: acc.hint ++ [get_hint]})
end)

acc ++ [Map.new([{:derive, derives}, {:field, field}, {:hint, hints}])]
end)
end

def pre_derives_check({{:ok, _, data}, _} = result, opts, field) do
run_pre_derives_check(data, opts[:derive], result, field, opts)
end

def pre_derives_check({{:ok, data}, _, _} = result, opts, field) do
run_pre_derives_check(data, opts[:derive], result, field, opts)
end

def pre_derives_check({{:error, _, _}, _} = result, _opts, _field), do: result

def pre_derives_check({{:error, _}, _, _} = result, _opts, _field), do: result

def pre_derives_check({{:error, _}, _} = result, _opts, _field), do: result

defp run_pre_derives_check(_, nil, validator_result, _field, _opts), do: validator_result

defp run_pre_derives_check(value, derive, _, field, opts) do
{:ok, Map.new([{field, value}]), [%{derive: derive, field: field}]}
|> derive()
|> case do
{:ok, data} -> {{:ok, field, Map.get(data, field)}, opts}
{:error, _} = error -> {error, field, opts}
end
end
end
Loading

0 comments on commit d986b6d

Please sign in to comment.