Skip to content

Commit

Permalink
feat: handle structs as data
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Aug 2, 2024
1 parent f8702b8 commit c738bac
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 10 deletions.
74 changes: 65 additions & 9 deletions lib/peri.ex
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,34 @@ defmodule Peri do
end
end

@doc """
Helper function to put a value into an enum, handling
not only maps and keyword lists but also structs.
## Examples
iex> Peri.put_in_enum(%{}, :hello, "world")
iex> Peri.put_in_enum(%{}, "hello", "world")
iex> Peri.put_in_enum(%User{}, :hello, "world")
iex> Peri.put_in_enum([], :hello, "world")
"""
def put_in_enum(enum, key, val) when is_struct(enum) do
struct(enum.__struct__, %{key => val})
end

def put_in_enum(enum, key, val) when is_map(enum) do
put_in(enum, [Access.key(key)], val)
end

def put_in_enum(enum, key, val) when is_list(enum) do
put_in(enum[key], val)
end

# if data is struct, well, we do not need to filter it
defp filter_data(_schema, data) when is_struct(data), do: data

defp filter_data(schema, data) do
acc = if is_map(schema), do: %{}, else: []
acc = make_filter_data_accumulator(schema, data)

Enum.reduce(schema, acc, fn {key, type}, acc ->
string_key = to_string(key)
Expand All @@ -287,10 +313,10 @@ defmodule Peri do

is_enumerable(value) and is_enumerable(type) ->
nested_filtered_value = filter_data(type, value)
put_in(acc[original_key], nested_filtered_value)
put_in_enum(acc, original_key, nested_filtered_value)

true ->
put_in(acc[original_key], value)
put_in_enum(acc, original_key, value)
end
end)
|> then(fn
Expand All @@ -299,8 +325,24 @@ defmodule Peri do
end)
end

defp enumerable_has_key?(data, key) when is_map(data) do
Map.has_key?(data, key) or Map.has_key?(data, (is_binary(key) && key) || Atom.to_string(key))
# we need to build structs after validating schema
defp make_filter_data_accumulator(_schema, data) when is_struct(data) do
%{__struct__: data.__struct__}
end

defp make_filter_data_accumulator(schema, _data) when is_map(schema), do: %{}
defp make_filter_data_accumulator(schema, _data) when is_list(schema), do: []

defp enumerable_has_key?(data, key) when is_struct(data) do
!!get_in(data, [Access.key(key)])
end

defp enumerable_has_key?(data, key) when is_map(data) and is_binary(key) do
Map.has_key?(data, key)
end

defp enumerable_has_key?(data, key) when is_map(data) and is_atom(key) do
Map.has_key?(data, key) or enumerable_has_key?(data, Atom.to_string(key))
end

defp enumerable_has_key?(data, key) when is_list(data) do
Expand Down Expand Up @@ -332,13 +374,27 @@ defmodule Peri do
end)
end

defp get_enumerable_value(enum, key) do
case Access.get(enum, key) do
nil when is_map(enum) -> Map.get(enum, (is_binary(key) && key) || Atom.to_string(key))
val -> val
# Access.key/1 only support maps and structs
def get_enumerable_value(enum, key) when is_struct(enum) do
get_in(enum, [Access.key(key)])
end

def get_enumerable_value(enum, key) when is_map(enum) and is_binary(key) do
Map.get(enum, key)
end

def get_enumerable_value(enum, key) when is_map(enum) and is_atom(key) do
if Map.has_key?(enum, key) do
Map.get(enum, key)
else
get_enumerable_value(enum, Atom.to_string(key))
end
end

def get_enumerable_value(enum, key) when is_list(enum) do
Keyword.get(enum, key)
end

@doc """
Checks if the given data is a numeric value, specifically a integer or a float.
Expand Down
2 changes: 1 addition & 1 deletion lib/peri/parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ defmodule Peri.Parser do
%Peri.Parser{data: %{name: "Alice", age: 30}, errors: [], path: []}
"""
def update_data(%__MODULE__{} = state, key, val) do
%{state | data: put_in(state.data[key], val).data}
%{state | data: Peri.put_in_enum(state.data, key, val)}
end

@doc """
Expand Down
36 changes: 36 additions & 0 deletions test/peri_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1877,4 +1877,40 @@ defmodule PeriTest do
] = errors
end
end

defmodule User do
defstruct [:name, :age, :email]
end

defschema(:user_map_schema, %{
name: {:required, :string},
age: :integer,
email: {:required, :string}
})

describe "basic struct input validation" do
test "validates struct input with valid data" do
data = %User{name: "John", age: 30, email: "john@example.com"}
assert {:ok, ^data} = user_map_schema(data)
end

test "validates struct input with missing required field" do
data = %User{name: "John", age: 30}

assert {:error, [%Peri.Error{path: [:email], message: "is required"}]} =
user_map_schema(data)
end

test "validates struct input with invalid field type" do
data = %User{name: "John", age: "thirty", email: "john@example.com"}

assert {:error,
[
%Peri.Error{
path: [:age],
message: "expected type of :integer received \"thirty\" value"
}
]} = user_map_schema(data)
end
end
end

0 comments on commit c738bac

Please sign in to comment.