Page MenuHomePhorge

No OneTemporary

Size
8 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/cast_error.ex b/lib/open_api_spex/cast_error.ex
index b3f6b8e..0b08c6a 100644
--- a/lib/open_api_spex/cast_error.ex
+++ b/lib/open_api_spex/cast_error.ex
@@ -1,67 +1,73 @@
defmodule OpenApiSpex.CastError do
+ alias OpenApiSpex.TermType
+
defstruct reason: nil,
value: nil,
format: nil,
type: nil,
name: nil,
path: []
def new(ctx, {:invalid_type, type}) do
%__MODULE__{reason: :invalid_type, type: type}
|> add_context_fields(ctx)
end
def new(ctx, {:invalid_format, format}) do
%__MODULE__{reason: :invalid_format, format: format}
|> add_context_fields(ctx)
end
def new(ctx, {:unexpected_field, name}) do
%__MODULE__{reason: :unexpected_field, name: name}
|> add_context_fields(ctx)
end
def new(ctx, {:missing_field, name}) do
%__MODULE__{reason: :missing_field, name: name}
|> add_context_fields(ctx)
end
def message(%{reason: :invalid_type, type: type, value: value} = ctx) do
- prepend_path("Invalid #{type}: #{inspect(value)}", ctx)
+ prepend_path("Invalid #{type}: #{inspect(TermType.type(value))}", ctx)
end
def message(%{reason: :polymorphic_failed, type: polymorphic_type} = ctx) do
prepend_path("Failed to cast to any schema in #{polymorphic_type}", ctx)
end
- def message(%{reason: :unexpected_field, value: value} = ctx) do
- prepend_path("Unexpected field with value #{inspect(value)}", ctx)
+ def message(%{reason: :unexpected_field, name: name} = ctx) do
+ prepend_path("Unexpected field: #{safe_string(name)}", ctx)
end
def message(%{reason: :no_value_required_for_discriminator, name: field} = ctx) do
prepend_path("No value for required disciminator property: #{field}", ctx)
end
def message(%{reason: :unknown_schema, name: name} = ctx) do
prepend_path("Unknown schema: #{name}", ctx)
end
def message(%{reason: :missing_field, name: name} = ctx) do
prepend_path("Missing field: #{name}", ctx)
end
defp add_context_fields(error, ctx) do
%{error | path: Enum.reverse(ctx.path), value: ctx.value}
end
defp prepend_path(message, ctx) do
path = "/" <> (ctx.path |> Enum.map(&to_string/1) |> Path.join())
"#" <> path <> ": " <> message
end
+
+ defp safe_string(string) do
+ to_string(string) |> String.slice(1..40)
+ end
end
defimpl String.Chars, for: OpenApiSpex.CastError do
def to_string(error) do
OpenApiSpex.CastError.message(error)
end
end
diff --git a/lib/open_api_spex/cast_object.ex b/lib/open_api_spex/cast_object.ex
index 00fff58..227b0fd 100644
--- a/lib/open_api_spex/cast_object.ex
+++ b/lib/open_api_spex/cast_object.ex
@@ -1,79 +1,83 @@
defmodule OpenApiSpex.CastObject do
@moduledoc false
alias OpenApiSpex.{Cast, CastContext}
+ def cast(%{value: value} = ctx) when not is_map(value) do
+ CastContext.error(ctx, {:invalid_type, :object})
+ end
+
def cast(%{value: value, schema: %{properties: nil}}) do
{:ok, value}
end
def cast(%{value: value, schema: schema} = ctx) do
schema_properties = schema.properties || %{}
with :ok <- check_unrecognized_properties(ctx, schema_properties),
value <- cast_atom_keys(value, schema_properties),
ctx = %{ctx | value: value},
:ok <- check_required_fields(ctx, schema),
:ok <- cast_properties(%{ctx | schema: schema_properties}) do
{:ok, value}
end
end
defp check_unrecognized_properties(%{value: value} = ctx, expected_keys) do
input_keys = value |> Map.keys() |> Enum.map(&to_string/1)
schema_keys = expected_keys |> Map.keys() |> Enum.map(&to_string/1)
extra_keys = input_keys -- schema_keys
if extra_keys == [] do
:ok
else
[name | _] = extra_keys
CastContext.error(ctx, {:unexpected_field, name})
end
end
defp check_required_fields(%{value: input_map} = ctx, schema) do
required = schema.required || []
input_keys = Map.keys(input_map)
missing_keys = required -- input_keys
if missing_keys == [] do
:ok
else
[key | _] = missing_keys
CastContext.error(ctx, {:missing_field, key})
end
end
defp cast_atom_keys(input_map, properties) do
Enum.reduce(properties, %{}, fn {key, _}, output ->
string_key = to_string(key)
case input_map do
%{^key => value} -> Map.put(output, key, value)
%{^string_key => value} -> Map.put(output, key, value)
_ -> output
end
end)
end
defp cast_properties(%{value: object, schema: schema_properties} = ctx) do
Enum.reduce(object, {:ok, %{}}, fn
{key, value}, {:ok, output} ->
cast_property(%{ctx | key: key, value: value, schema: schema_properties}, output)
_, error ->
error
end)
end
defp cast_property(%{key: key, schema: schema_properties} = ctx, output) do
prop_schema = Map.get(schema_properties, key)
path = [key | ctx.path]
with {:error, errors} <- Cast.cast(%{ctx | path: path, schema: prop_schema}) do
{:error, errors}
else
{:ok, value} -> {:ok, Map.put(output, key, value)}
end
end
end
diff --git a/lib/open_api_spex/term_type.ex b/lib/open_api_spex/term_type.ex
new file mode 100644
index 0000000..0b40914
--- /dev/null
+++ b/lib/open_api_spex/term_type.ex
@@ -0,0 +1,13 @@
+defmodule OpenApiSpex.TermType do
+ alias OpenApiSpex.Schema
+
+ @spec type(term) :: Schema.data_type() | nil | String.t()
+ def type(v) when is_list(v), do: :array
+ def type(v) when is_map(v), do: :object
+ def type(v) when is_binary(v), do: :string
+ def type(v) when is_boolean(v), do: :boolean
+ def type(v) when is_integer(v), do: :integer
+ def type(v) when is_number(v), do: :number
+ def type(v) when is_nil(v), do: nil
+ def type(_), do: :unknown
+end
diff --git a/test/cast_object_test.exs b/test/cast_object_test.exs
index a6d9c64..53e9e58 100644
--- a/test/cast_object_test.exs
+++ b/test/cast_object_test.exs
@@ -1,59 +1,67 @@
defmodule OpenApiSpex.CastObjectTest do
use ExUnit.Case
alias OpenApiSpex.{CastContext, CastObject, CastError, Schema}
defp cast(ctx), do: CastObject.cast(struct(CastContext, ctx))
describe "cast/3" do
+ test "not an object" do
+ schema = %Schema{type: :object}
+ assert {:error, [error]} = cast(value: ["hello"], schema: schema)
+ assert %CastError{} = error
+ assert error.reason == :invalid_type
+ assert error.value == ["hello"]
+ end
+
test "properties:nil, given unknown input property" do
schema = %Schema{type: :object}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert cast(value: %{"unknown" => "hello"}, schema: schema) ==
{:ok, %{"unknown" => "hello"}}
end
test "with empty schema properties, given unknown input property" do
schema = %Schema{type: :object, properties: %{}}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert {:error, [error]} = cast(value: %{"unknown" => "hello"}, schema: schema)
assert %CastError{} = error
end
test "with schema properties set, given known input property" do
schema = %Schema{
type: :object,
properties: %{age: nil}
}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert cast(value: %{"age" => "hello"}, schema: schema) == {:ok, %{age: "hello"}}
end
test "required fields" do
schema = %Schema{
type: :object,
properties: %{age: nil},
required: [:age]
}
assert {:error, [error]} = cast(value: %{}, schema: schema)
assert %CastError{} = error
assert error.reason == :missing_field
assert error.name == :age
end
test "cast property against schema" do
schema = %Schema{
type: :object,
properties: %{age: %Schema{type: :integer}}
}
assert cast(value: %{}, schema: schema) == {:ok, %{}}
assert {:error, [error]} = cast(value: %{"age" => "hello"}, schema: schema)
assert %CastError{} = error
assert error.reason == :invalid_type
assert error.path == [:age]
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 30, 5:37 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41505
Default Alt Text
(8 KB)

Event Timeline