Skip to content

Commit

Permalink
Add strict as a global attribute to generated structs
Browse files Browse the repository at this point in the history
  • Loading branch information
abelino committed Oct 8, 2024
1 parent 2045694 commit f2dad4c
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 9 deletions.
10 changes: 9 additions & 1 deletion lib/mix/tasks/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,20 @@ defmodule Mix.Tasks.Compile.Speck do

{:__block__, _, top_ast} = file_ast

context = Enum.reduce(top_ast, %{attributes: []}, fn
initial_context =
%{attributes: [], strict: false}

context = Enum.reduce(top_ast, initial_context, fn
{:struct, _, [{:__aliases__, _, _namespace} = module_ast]}, acc ->
{module, _} = Code.eval_quoted(module_ast)
Map.put(acc, :module, module)

{:name, _, [name]}, acc ->
Map.put(acc, :name, name)

{:strict, _, [strict]}, acc ->
Map.put(acc, :strict, !!strict)

{:attribute, _, _} = attribute_ast, acc ->
attributes = acc.attributes ++ [build_attribute(attribute_ast)]
Map.put(acc, :attributes, attributes)
Expand All @@ -136,6 +142,8 @@ defmodule Mix.Tasks.Compile.Speck do
@moduledoc false
defstruct unquote(Macro.escape(top_level_attribute_names))

def strict, do: unquote(Macro.escape(context.strict))

def attributes, do: unquote(Macro.escape(context.attributes))
end
end
Expand Down
23 changes: 15 additions & 8 deletions lib/speck.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ defmodule Speck do
"""
@spec validate(schema :: module, params :: map) :: {:ok, struct}, {:error, map}
def validate(schema, params) do
case do_validate(:map, params, [], schema.attributes) do
opts = [strict: schema.strict]

case do_validate(:map, params, opts, schema.attributes) do
{fields, errors} when errors == %{} ->
struct = struct(schema, fields)
{:ok, struct}
Expand Down Expand Up @@ -111,14 +113,15 @@ defmodule Speck do
defp do_strict_validate(:datetime, %DateTime{} = value, _opts), do: value
defp do_strict_validate(_type, _value, _opts), do: {:error, :wrong_type}

defp do_validate(:map, value, _opts, attributes) do
defp do_validate(:map, value, global_opts, attributes) do
Enum.reduce(attributes, {%{}, %{}}, fn
{name, [:map], opts, attributes}, {fields, errors} ->
raw_values = get_raw_value(value, name) || []
merged_opts = Keyword.merge(global_opts, opts)
raw_values = get_raw_value(value, name) || []

coerced_maplist =
raw_values
|> Enum.map(&do_validate(:map, &1, opts, attributes))
|> Enum.map(&do_validate(:map, &1, merged_opts, attributes))
|> Enum.with_index
|> Enum.reduce({_values = [], _errors = []}, fn
{{value, error}, _index}, {values, errors}
Expand Down Expand Up @@ -146,12 +149,16 @@ defmodule Speck do
end

{name, :map, opts, attributes}, {fields, errors} ->
raw_value = get_raw_value(value, name)
apply_filters(name, :map, raw_value, opts, fields, errors, attributes)
merged_opts = Keyword.merge(global_opts, opts)
raw_value = get_raw_value(value, name)

apply_filters(name, :map, raw_value, merged_opts, fields, errors, attributes)

{name, type, opts}, {fields, errors} ->
raw_value = get_raw_value(value, name)
apply_filters(name, type, raw_value, opts, fields, errors, nil)
merged_opts = Keyword.merge(global_opts, opts)
raw_value = get_raw_value(value, name)

apply_filters(name, type, raw_value, merged_opts, fields, errors, nil)
end)
end

Expand Down
15 changes: 15 additions & 0 deletions protocol/test/strict.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
struct TestSchema.Strict

name "strict"

strict true

attribute :param1, :integer
attribute :param2, :float
attribute :param3, :boolean
attribute :param4, :string
attribute :param5, :string, strict: false
attribute :param6, :atom
attribute :param7, :datetime
attribute :param8, :date
attribute :param9, :time
53 changes: 53 additions & 0 deletions test/speck_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,59 @@ defmodule Speck.Test do
}}
end

test "can coerce a struct when values pass strict validation" do
params = %{
"param1" => 1,
"param2" => 2.0,
"param3" => true,
"param4" => "valid",
"param5" => 1,
"param6" => :valid,
"param7" => ~U[1970-01-01 00:00:00Z],
"param8" => ~D[1970-01-01],
"param9" => ~T[00:00:00],
}

assert Speck.validate(TestSchema.Strict, params) ==
{:ok, %TestSchema.Strict{
param1: 1,
param2: 2.0,
param3: true,
param4: "valid",
param5: "1",
param6: :valid,
param7: ~U[1970-01-01 00:00:00Z],
param8: ~D[1970-01-01],
param9: ~T[00:00:00],
}}
end

test "returns error if strict value is the wrong type" do
params = %{
"param1" => "invalid",
"param2" => "invalid",
"param3" => "invalid",
"param4" => 2.7,
"param5" => 2.7,
"param6" => 2.7,
"param7" => 2.7,
"param8" => %{},
"param9" => {:type, :ipv4}
}

assert Speck.validate(TestSchema.Strict, params) ==
{:error, %{
param1: :wrong_type,
param2: :wrong_type,
param3: :wrong_type,
param4: :wrong_type,
param6: :wrong_type,
param7: :wrong_type,
param8: :wrong_type,
param9: :wrong_type,
}}
end

test "falsy values coerce successfully" do
params = %{
"param1" => false,
Expand Down

0 comments on commit f2dad4c

Please sign in to comment.