Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F115926
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
16 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/open_api_spex/cast.ex b/lib/open_api_spex/cast.ex
index 35e198d..f9b155e 100644
--- a/lib/open_api_spex/cast.ex
+++ b/lib/open_api_spex/cast.ex
@@ -1,193 +1,189 @@
defmodule OpenApiSpex.Cast do
alias OpenApiSpex.{Reference, Schema}
alias OpenApiSpex.Reference
alias OpenApiSpex.Cast.{
AllOf,
AnyOf,
Array,
Discriminator,
Error,
Integer,
Object,
OneOf,
Primitive,
String
}
@type schema_or_reference :: Schema.t() | Reference.t()
@type t :: %__MODULE__{
value: term(),
schema: schema_or_reference | nil,
schemas: map(),
path: [atom() | String.t() | integer()],
key: atom() | nil,
index: integer,
errors: [Error.t()]
}
defstruct value: nil,
schema: nil,
schemas: %{},
path: [],
key: nil,
index: 0,
errors: []
@doc ~S"""
Cast and validate a value against the given schema.
Recognizes all the types defined in Open API (itself a superset of JSON Schema).
JSON Schema types:
[https://json-schema.org/latest/json-schema-core.html#rfc.section.4.2.1](https://json-schema.org/latest/json-schema-core.html#rfc.section.4.2.1)
Open API primitive types:
[https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#data-types](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#data-types)
For an `:object` schema type, the cast operation returns a map with atom keys.
## Examples
iex> alias OpenApiSpex.{Cast, Schema}
iex> schema = %Schema{type: :string}
iex> Cast.cast(schema, "a string")
{:ok, "a string"}
iex> Cast.cast(schema, :not_a_string)
{
:error,
[
%OpenApiSpex.Cast.Error{
reason: :invalid_type,
type: :string,
value: :not_a_string
}
]
}
iex> schema = %Schema{
...> type: :object,
...> properties: %{
...> name: nil
...> }
...> }
iex> Cast.cast(schema, %{"name" => "spex"})
{:ok, %{name: "spex"}}
iex> Cast.cast(schema, %{"bad" => "spex"})
{
:error,
[
%OpenApiSpex.Cast.Error{
name: "bad",
path: ["bad"],
reason: :unexpected_field,
value: %{"bad" => "spex"}
}
]
}
"""
@spec cast(schema_or_reference | nil, term(), map()) :: {:ok, term()} | {:error, [Error.t()]}
def cast(schema, value, schemas \\ %{}) do
ctx = %__MODULE__{schema: schema, value: value, schemas: schemas}
cast(ctx)
end
@spec cast(t()) :: {:ok, term()} | {:error, [Error.t()]}
# nil schema
def cast(%__MODULE__{value: value, schema: nil}),
do: {:ok, value}
def cast(%__MODULE__{schema: %Reference{}} = ctx) do
schema = Reference.resolve_schema(ctx.schema, ctx.schemas)
cast(%{ctx | schema: schema})
end
# nullable: true
def cast(%__MODULE__{value: nil, schema: %{nullable: true}}) do
{:ok, nil}
end
# nullable: false
def cast(%__MODULE__{value: nil} = ctx) do
error(ctx, {:null_value})
end
# Enum
def cast(%__MODULE__{schema: %{enum: []}} = ctx) do
cast(%{ctx | schema: %{ctx.schema | enum: nil}})
end
# Enum
def cast(%__MODULE__{schema: %{enum: enum}} = ctx) when is_list(enum) do
with {:ok, value} <- cast(%{ctx | schema: %{ctx.schema | enum: nil}}) do
- if value in enum do
- {:ok, value}
- else
- error(ctx, {:invalid_enum})
- end
+ OpenApiSpex.Cast.Enum.cast(%{ctx | value: value})
end
end
## Specific types
def cast(%__MODULE__{schema: %{type: :object, discriminator: discriminator}} = ctx)
when is_map(discriminator),
do: Discriminator.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, anyOf: schemas}} = ctx) when is_list(schemas),
do: AnyOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, allOf: schemas}} = ctx) when is_list(schemas),
do: AllOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: _, oneOf: schemas}} = ctx) when is_list(schemas),
do: OneOf.cast(ctx)
def cast(%__MODULE__{schema: %{type: :object}} = ctx),
do: Object.cast(ctx)
def cast(%__MODULE__{schema: %{type: :boolean}} = ctx),
do: Primitive.cast_boolean(ctx)
def cast(%__MODULE__{schema: %{type: :integer}} = ctx),
do: Integer.cast(ctx)
def cast(%__MODULE__{schema: %{type: :number}} = ctx),
do: Primitive.cast_number(ctx)
def cast(%__MODULE__{schema: %{type: :string}} = ctx),
do: String.cast(ctx)
def cast(%__MODULE__{schema: %{type: :array}} = ctx),
do: Array.cast(ctx)
def cast(%__MODULE__{schema: %{type: _other}} = ctx),
do: error(ctx, {:invalid_schema_type})
def cast(%{} = ctx), do: cast(struct(__MODULE__, ctx))
def cast(ctx) when is_list(ctx), do: cast(struct(__MODULE__, ctx))
# Add an error
def error(ctx, error_args) do
error = Error.new(ctx, error_args)
{:error, [error | ctx.errors]}
end
def ok(%__MODULE__{value: value}), do: {:ok, value}
def success(%__MODULE__{schema: schema} = ctx, schema_properties)
when is_list(schema_properties) do
schema_without_successful_validation_property =
Enum.reduce(schema_properties, schema, fn property, schema ->
%{schema | property => nil}
end)
{:cast, %{ctx | schema: schema_without_successful_validation_property}}
end
def success(%__MODULE__{schema: _schema} = ctx, schema_property) do
success(ctx, [schema_property])
end
end
diff --git a/lib/open_api_spex/cast/enum.ex b/lib/open_api_spex/cast/enum.ex
new file mode 100644
index 0000000..b78c146
--- /dev/null
+++ b/lib/open_api_spex/cast/enum.ex
@@ -0,0 +1,30 @@
+defmodule OpenApiSpex.Cast.Enum do
+ @moduledoc false
+ alias OpenApiSpex.Cast
+
+ def cast(ctx = %Cast{schema: %{enum: enum}, value: value}) do
+ case Enum.find(enum, {:error, :invalid_enum}, &equivalent?(&1, value)) do
+ {:error, :invalid_enum} -> Cast.error(ctx, {:invalid_enum})
+ found -> {:ok, found}
+ end
+ end
+
+ defp equivalent?(x, x), do: true
+
+ # Special case: atoms are equivalent to their stringified representation
+ defp equivalent?(left, right) when is_atom(left) and is_binary(right) do
+ to_string(left) == right
+ end
+
+ # an explicit schema should be used to cast to enum of structs
+ defp equivalent?(_x, %_struct{}), do: false
+
+ # Special case: Atom-keyed maps are equivalent to their string-keyed representation
+ defp equivalent?(left, right) when is_map(left) and is_map(right) do
+ Enum.all?(left, fn {k, v} ->
+ equivalent?(v, Map.get(right, to_string(k)))
+ end)
+ end
+
+ defp equivalent?(_left, _right), do: false
+end
diff --git a/test/cast/enum_test.exs b/test/cast/enum_test.exs
new file mode 100644
index 0000000..7f78df9
--- /dev/null
+++ b/test/cast/enum_test.exs
@@ -0,0 +1,90 @@
+defmodule OpenApiSpex.Cast.EnumTest do
+ use ExUnit.Case
+ alias OpenApiSpex.{Cast, Schema}
+ alias OpenApiSpex.Cast.Error
+
+ defp cast(ctx), do: Cast.cast(ctx)
+
+ defmodule User do
+ require OpenApiSpex
+ alias __MODULE__
+
+ defstruct [:age]
+
+ def schema() do
+ %OpenApiSpex.Schema{
+ type: :object,
+ required: [:age],
+ properties: %{
+ age: %Schema{type: :integer},
+ },
+ enum: [%User{age: 32}, %User{age: 45}],
+ "x-struct": __MODULE__
+ }
+ end
+ end
+
+ describe "Enum of strings" do
+ setup do
+ {:ok, %{schema: %Schema{type: :string, enum: ["one"]}}}
+ end
+
+ test "error on invalid string", %{schema: schema} do
+ assert {:error, [error]} = cast(schema: schema, value: "two")
+ assert %Error{} = error
+ assert error.reason == :invalid_enum
+ end
+
+ test "OK on valid string", %{schema: schema} do
+ assert {:ok, "one"} = cast(schema: schema, value: "one")
+ end
+ end
+
+ describe "Enum of atoms" do
+ setup do
+ {:ok, %{schema: %Schema{type: :string, enum: [:one, :two, :three]}}}
+ end
+
+ test "string will be converted to atom", %{schema: schema} do
+ assert {:ok, :three} = cast(schema: schema, value: "three")
+ end
+
+ test "error on invalid string", %{schema: schema} do
+ assert {:error, [error]} = cast(schema: schema, value: "four")
+ assert %Error{} = error
+ assert error.reason == :invalid_enum
+ end
+ end
+
+ describe "Enum with explicit schema" do
+ test "converts string keyed map to struct" do
+ assert {:ok, %User{age: 32}} = cast(schema: User.schema(), value: %{"age" => 32})
+ end
+
+ test "Must be a valid enum value" do
+ assert {:error, [error]} = cast(schema: User.schema(), value: %{"age" => 33})
+ assert %Error{} = error
+ assert error.reason == :invalid_enum
+ end
+ end
+
+ describe "Enum without explicit schema" do
+ setup do
+ schema = %Schema{
+ type: :object,
+ enum: [%{age: 55}, %{age: 66}, %{age: 77}]
+ }
+ {:ok, %{schema: schema}}
+ end
+
+ test "casts from string keyed map", %{schema: schema} do
+ assert {:ok, %{age: 55}} = cast(value: %{"age" => 55}, schema: schema)
+ end
+
+ test "value must be a valid enum value", %{schema: schema} do
+ assert {:error, [error]} = cast(value: %{"age" => 56}, schema: schema)
+ assert %Error{} = error
+ assert error.reason == :invalid_enum
+ end
+ end
+end
diff --git a/test/cast_test.exs b/test/cast_test.exs
index 2d7ecd6..5e41091 100644
--- a/test/cast_test.exs
+++ b/test/cast_test.exs
@@ -1,232 +1,219 @@
defmodule OpenApiSpec.CastTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, Schema, Reference}
alias OpenApiSpex.Cast.Error
doctest OpenApiSpex.Cast
def cast(ctx), do: Cast.cast(ctx)
describe "cast/1" do
test "unknown schema type" do
assert {:error, [error]} = cast(value: "string", schema: %Schema{type: :nope})
assert error.reason == :invalid_schema_type
assert error.type == :nope
assert {:error, [error]} = cast(value: "string", schema: %Schema{type: nil})
assert error.reason == :invalid_schema_type
assert error.type == nil
end
# Note: full tests for primitives are covered in Cast.PrimitiveTest
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 Error.message_with_path(error) == "#: null value where array expected"
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 %Error{} = 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 [%Error{} = error] = errors
assert error.reason == :invalid_type
assert error.value == "two"
assert error.path == [1]
end
# Additional object tests found in Cast.ObjectTest
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 %Error{} = 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 %Error{} = error
assert error.path == [:data, :age]
assert Error.message_with_path(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 %Error{} = error
assert error.path == [:data, 1, :age]
assert Error.message_with_path(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 %Error{} = error
assert error.reason == :invalid_type
assert error.path == [1]
assert Error.message_with_path(error) == "#/1: Invalid integer. Got: string"
assert Error.message_with_path(error2) == "#/3: Invalid integer. Got: string"
end
-
- test "enum - invalid" do
- schema = %Schema{type: :string, enum: ["one"]}
- assert {:error, [error]} = cast(value: "two", schema: schema)
-
- assert %Error{} = error
- assert error.reason == :invalid_enum
- end
-
- test "enum - valid" do
- schema = %Schema{type: :string, enum: ["one"]}
- assert {:ok, "one"} = cast(value: "one", schema: schema)
- end
end
describe "ok/1" do
test "basics" do
assert {:ok, 1} = Cast.ok(%Cast{value: 1})
end
end
describe "success/2" do
test "nils out property" do
schema = %Schema{minimum: 1}
ctx = %Cast{schema: schema}
expected = {:cast, %Cast{schema: %Schema{minimum: nil}}}
assert expected == Cast.success(ctx, :minimum)
end
test "nils out properties" do
schema = %Schema{minimum: 1, exclusiveMinimum: true}
ctx = %Cast{schema: schema}
expected = {:cast, %Cast{schema: %Schema{minimum: nil, exclusiveMinimum: nil}}}
assert expected == Cast.success(ctx, [:minimum, :exclusiveMinimum])
end
end
end
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 29, 4:29 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41256
Default Alt Text
(16 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment