Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/cast.ex b/lib/open_api_spex/cast.ex
index f8f3f70..10b4f47 100644
--- a/lib/open_api_spex/cast.ex
+++ b/lib/open_api_spex/cast.ex
@@ -1,31 +1,41 @@
defmodule OpenApiSpex.Cast do
alias OpenApiSpex.{CastArray, CastContext, CastObject, CastPrimitive, Reference}
@primitives [:boolean, :integer, :number, :string]
def cast(schema, value, schemas) do
ctx = %CastContext{schema: schema, value: value, schemas: schemas}
with {:error, [error | _]} <- cast(ctx) do
{:error, to_string(error)}
end
end
def cast(%CastContext{value: value, schema: nil}),
do: {:ok, value}
def cast(%CastContext{schema: %Reference{}} = ctx) do
schema = Reference.resolve_schema(ctx.schema, ctx.schemas)
cast(%{ctx | schema: schema})
end
+ def cast(%CastContext{value: nil, schema: %{nullable: true}}) do
+ {:ok, nil}
+ end
+
+ def cast(%CastContext{value: nil} = ctx) do
+ CastContext.error(ctx, {:null_value})
+ end
+
+ # Specific types
+
def cast(%CastContext{schema: %{type: type}} = ctx) when type in @primitives,
do: CastPrimitive.cast(ctx)
def cast(%CastContext{schema: %{type: :array}} = ctx),
do: CastArray.cast(ctx)
def cast(%CastContext{schema: %{type: :object}} = ctx),
do: CastObject.cast(ctx)
def cast(%{} = ctx), do: cast(struct(CastContext, ctx))
end
diff --git a/lib/open_api_spex/cast_error.ex b/lib/open_api_spex/cast_error.ex
index 0ff9f61..b9c9603 100644
--- a/lib/open_api_spex/cast_error.ex
+++ b/lib/open_api_spex/cast_error.ex
@@ -1,73 +1,87 @@
defmodule OpenApiSpex.CastError do
alias OpenApiSpex.TermType
defstruct reason: nil,
value: nil,
format: nil,
type: nil,
name: nil,
path: []
+ def new(ctx, {:null_value}) do
+ %__MODULE__{reason: :null_value}
+ |> add_context_fields(ctx)
+ end
+
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: :null_value} = ctx) do
+ prepend_path("Null value", ctx)
+ end
+
def message(%{reason: :invalid_type, type: type, value: value} = ctx) do
prepend_path("Invalid #{type}. Got: #{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, 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
+ path =
+ case ctx.path do
+ [] -> "#"
+ _ -> "#/" <> (ctx.path |> Enum.map(&to_string/1) |> Path.join())
+ end
+
+ 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_primitive.ex b/lib/open_api_spex/cast_primitive.ex
index 453edf7..03f5e14 100644
--- a/lib/open_api_spex/cast_primitive.ex
+++ b/lib/open_api_spex/cast_primitive.ex
@@ -1,87 +1,84 @@
defmodule OpenApiSpex.CastPrimitive do
@moduledoc false
alias OpenApiSpex.CastContext
- def cast(%{value: nil, schema: %{nullable: true}}),
- do: {:ok, nil}
-
def cast(%{schema: %{type: :boolean}} = ctx),
do: cast_boolean(ctx)
def cast(%{schema: %{type: :integer}} = ctx),
do: cast_integer(ctx)
def cast(%{schema: %{type: :number}} = ctx),
do: cast_number(ctx)
def cast(%{schema: %{type: :string}} = ctx),
do: cast_string(ctx)
## Private functions
defp cast_boolean(%{value: value}) when is_boolean(value) do
{:ok, value}
end
defp cast_boolean(%{value: "true"}), do: {:ok, true}
defp cast_boolean(%{value: "false"}), do: {:ok, false}
defp cast_boolean(ctx) do
CastContext.error(ctx, {:invalid_type, :boolean})
end
defp cast_integer(%{value: value}) when is_integer(value) do
{:ok, value}
end
defp cast_integer(%{value: value}) when is_number(value) do
{:ok, round(value)}
end
defp cast_integer(%{value: value} = ctx) when is_binary(value) do
case Float.parse(value) do
{value, ""} -> cast_integer(%{ctx | value: value})
_ -> CastContext.error(ctx, {:invalid_type, :integer})
end
end
defp cast_integer(ctx) do
CastContext.error(ctx, {:invalid_type, :integer})
end
defp cast_number(%{value: value}) when is_number(value) do
{:ok, value}
end
defp cast_number(%{value: value}) when is_integer(value) do
{:ok, value / 1}
end
defp cast_number(%{value: value} = ctx) when is_binary(value) do
case Float.parse(value) do
{value, ""} -> {:ok, value}
_ -> CastContext.error(ctx, {:invalid_type, :number})
end
end
defp cast_number(ctx) do
CastContext.error(ctx, {:invalid_type, :number})
end
defp cast_string(%{value: value, schema: %{pattern: pattern}} = ctx)
when not is_nil(pattern) and is_binary(value) do
if Regex.match?(pattern, value) do
{:ok, value}
else
CastContext.error(ctx, {:invalid_format, pattern})
end
end
defp cast_string(%{value: value}) when is_binary(value) do
{:ok, value}
end
defp cast_string(ctx) do
CastContext.error(ctx, {:invalid_type, :string})
end
end
diff --git a/test/cast_primitive_test.exs b/test/cast_primitive_test.exs
index f51e3d0..1fe9aba 100644
--- a/test/cast_primitive_test.exs
+++ b/test/cast_primitive_test.exs
@@ -1,76 +1,59 @@
defmodule OpenApiSpex.CastPrimitiveTest do
use ExUnit.Case
alias OpenApiSpex.{CastContext, CastPrimitive, CastError, Schema}
defp cast(ctx), do: CastPrimitive.cast(struct(CastContext, ctx))
describe "cast/3" do
- @types [:boolean, :integer, :number, :string]
- for type <- @types do
- @type_value type
- test "nil input for nullable:true, type:#{type}" do
- schema = %Schema{type: @type_value, nullable: true}
- assert cast(value: nil, schema: schema) == {:ok, nil}
- end
-
- test "nil input for nullable:false, type:#{type}" do
- schema = %Schema{type: @type_value, nullable: false}
- assert {:error, [error]} = cast(value: nil, schema: schema)
- assert %CastError{} = error
- assert error.reason == :invalid_type
- assert error.value == nil
- end
- end
-
test "boolean" do
schema = %Schema{type: :boolean}
assert cast(value: true, schema: schema) == {:ok, true}
assert cast(value: false, schema: schema) == {:ok, false}
assert cast(value: "true", schema: schema) == {:ok, true}
assert cast(value: "false", schema: schema) == {:ok, false}
assert {:error, [error]} = cast(value: "other", schema: schema)
assert %CastError{reason: :invalid_type} = error
assert error.value == "other"
end
test "integer" do
schema = %Schema{type: :integer}
assert cast(value: 1, schema: schema) == {:ok, 1}
assert cast(value: 1.5, schema: schema) == {:ok, 2}
assert cast(value: "1", schema: schema) == {:ok, 1}
assert cast(value: "1.5", schema: schema) == {:ok, 2}
assert {:error, [error]} = cast(value: "other", schema: schema)
assert %CastError{reason: :invalid_type} = error
assert error.value == "other"
end
test "number" do
schema = %Schema{type: :number}
assert cast(value: 1, schema: schema) == {:ok, 1.0}
assert cast(value: 1.5, schema: schema) == {:ok, 1.5}
assert cast(value: "1", schema: schema) == {:ok, 1.0}
assert cast(value: "1.5", schema: schema) == {:ok, 1.5}
assert {:error, [error]} = cast(value: "other", schema: schema)
assert %CastError{reason: :invalid_type} = error
assert error.value == "other"
end
test "string" do
schema = %Schema{type: :string}
assert cast(value: "hello", schema: schema) == {:ok, "hello"}
assert cast(value: "", schema: schema) == {:ok, ""}
assert {:error, [error]} = cast(value: %{}, schema: schema)
assert %CastError{reason: :invalid_type} = error
assert error.value == %{}
end
test "string with pattern" do
schema = %Schema{type: :string, pattern: ~r/\d-\d/}
assert cast(value: "1-2", schema: schema) == {:ok, "1-2"}
assert {:error, [error]} = cast(value: "hello", schema: schema)
assert error.reason == :invalid_format
assert error.value == "hello"
assert error.format == ~r/\d-\d/
end
end
end
diff --git a/test/cast_test.exs b/test/cast_test.exs
index 2239da3..adffa3b 100644
--- a/test/cast_test.exs
+++ b/test/cast_test.exs
@@ -1,171 +1,183 @@
defmodule OpenApiSpec.CastTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, CastContext, CastError, Reference, Schema}
def cast(ctx), do: Cast.cast(struct(CastContext, ctx))
describe "cast/3" do
# Note: full tests for primitives are covered in CastPrimitiveTest
test "primitives" do
tests = [
{:string, "1", :ok},
{:string, "", :ok},
{:string, true, :invalid},
{:string, nil, :invalid},
{:integer, 1, :ok},
{:integer, "1", :ok},
{:integer, %{}, :invalid},
{:integer, nil, :invalid},
{:array, nil, :invalid},
{:object, nil, :invalid}
]
for {type, input, expected} <- tests do
case expected do
:ok -> assert {:ok, _} = cast(value: input, schema: %Schema{type: type})
:invalid -> assert {:error, _} = cast(value: input, schema: %Schema{type: type})
end
end
end
+ test "array type, nullable, given nil" do
+ schema = %Schema{type: :array, nullable: true}
+ assert {:ok, nil} = cast(value: nil, schema: schema)
+ end
+
+ test "array type, given nil" do
+ schema = %Schema{type: :array}
+ assert {:error, [error]} = cast(value: nil, schema: schema)
+ assert error.reason == :null_value
+ assert to_string(error) == "#: Null value"
+ end
+
test "array" do
schema = %Schema{type: :array}
assert cast(value: [], schema: schema) == {:ok, []}
assert cast(value: [1, 2, 3], schema: schema) == {:ok, [1, 2, 3]}
assert cast(value: ["1", "2", "3"], schema: schema) == {:ok, ["1", "2", "3"]}
assert {:error, [error]} = cast(value: %{}, schema: schema)
assert %CastError{} = error
assert error.reason == :invalid_type
assert error.value == %{}
end
test "array with items schema" do
items_schema = %Schema{type: :integer}
schema = %Schema{type: :array, items: items_schema}
assert cast(value: [], schema: schema) == {:ok, []}
assert cast(value: [1, 2, 3], schema: schema) == {:ok, [1, 2, 3]}
assert cast(value: ["1", "2", "3"], schema: schema) == {:ok, [1, 2, 3]}
assert {:error, errors} = cast(value: [1, "two"], schema: schema)
assert [%CastError{} = error] = errors
assert error.reason == :invalid_type
assert error.value == "two"
assert error.path == [1]
end
# Additional object tests found in CastObjectTest
test "object 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 "reference" do
age_schema = %Schema{type: :integer}
assert cast(
value: "20",
schema: %Reference{"$ref": "#/components/schemas/Age"},
schemas: %{"Age" => age_schema}
) == {:ok, 20}
end
test "reference nested in object" do
age_schema = %Schema{type: :integer}
schema = %Schema{
type: :object,
properties: %{
age: %Reference{"$ref": "#/components/schemas/Age"}
}
}
assert cast(
value: %{"age" => "20"},
schema: schema,
schemas: %{"Age" => age_schema}
) == {:ok, %{age: 20}}
end
test "paths" do
schema = %Schema{
type: :object,
properties: %{
age: %Schema{type: :integer}
}
}
assert {:error, errors} = cast(value: %{"age" => "twenty"}, schema: schema)
assert [error] = errors
assert %CastError{} = error
assert error.path == [:age]
end
test "nested paths" do
schema = %Schema{
type: :object,
properties: %{
data: %Schema{
type: :object,
properties: %{
age: %Schema{type: :integer}
}
}
}
}
assert {:error, errors} = cast(value: %{"data" => %{"age" => "twenty"}}, schema: schema)
assert [error] = errors
assert %CastError{} = error
assert error.path == [:data, :age]
assert to_string(error) == "#/data/age: Invalid integer. Got: string"
end
test "paths involving arrays" do
schema = %Schema{
type: :object,
properties: %{
data: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
age: %Schema{type: :integer}
}
}
}
}
}
assert {:error, errors} =
cast(value: %{"data" => [%{"age" => "20"}, %{"age" => "twenty"}]}, schema: schema)
assert [error] = errors
assert %CastError{} = error
assert error.path == [:data, 1, :age]
assert to_string(error) == "#/data/1/age: Invalid integer. Got: string"
end
test "multiple errors" do
schema = %Schema{
type: :array,
items: %Schema{type: :integer}
}
value = [1, "two", 3, "four"]
assert {:error, errors} = cast(value: value, schema: schema)
assert [error, error2] = errors
assert %CastError{} = error
assert error.reason == :invalid_type
assert error.path == [1]
assert to_string(error) == "#/1: Invalid integer. Got: string"
assert to_string(error2) == "#/3: Invalid integer. Got: string"
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 30, 4:46 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41500
Default Alt Text
(15 KB)

Event Timeline