diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index d8959a15..deb9d177 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -421,16 +421,49 @@ defmodule OpenApiSpex.Schema do :ok iex> OpenApiSpex.Schema.validate(%OpenApiSpex.Schema{type: :string, pattern: "(.*)@(.*)"}, "joegmail.com", %{}) - {:error, "#: Value does not match pattern: (.*)@(.*)"} + {:error, "#: Value \\"joegmail.com\\" does not match pattern: (.*)@(.*)"} """ @spec validate(Schema.t | Reference.t, any, %{String.t => Schema.t | Reference.t}) :: :ok | {:error, String.t} def validate(schema, val, schemas), do: validate(schema, val, "#", schemas) @spec validate(Schema.t | Reference.t, any, String.t, %{String.t => Schema.t | Reference.t}) :: :ok | {:error, String.t} def validate(ref = %Reference{}, val, path, schemas), do: validate(Reference.resolve_schema(ref, schemas), val, path, schemas) - def validate(%Schema{nullable: true}, nil, _path, _schemas), do: :ok - def validate(%Schema{type: type}, nil, path, _schemas) do - {:error, "#{path}: null value where #{type} expected"} + def validate(%Schema{oneOf: schemasOf = [_|_]}, value, path, schemas) do + case Enum.count(schemasOf, fn schema -> :ok == validate(schema, value, path, schemas) end) do + 1 -> :ok + 0 -> {:error, "#{path}: Not one schema matches \"oneOf\": #{inspect(value)}"} + _ -> {:error, "#{path}: More than one schema matches \"oneOf\": #{inspect(value)}"} + end + end + def validate(%Schema{anyOf: schemasOf = [_|_]}, value, path, schemas) do + if Enum.any?(schemasOf, fn schema -> :ok == validate(schema, value, path, schemas) end) do + :ok + else + {:error, "#{path}: Not one schema matches \"anyOf\": #{inspect(value)}"} + end + end + def validate(%Schema{allOf: schemasOf = [_|_]}, value, path, schemas) do + if Enum.all?(schemasOf, fn schema -> :ok == validate(schema, value, path, schemas) end) do + :ok + else + {:error, "#{path}: At least one schema does not match \"allOf\": #{inspect(value)}"} + end + end + def validate(%Schema{not: schema}, value, path, schemas) when schema != nil do + case validate(schema, value, path, schemas) do + {:error, _} -> :ok + :ok -> {:error, "#{path}: Schema should \"not\" be matching: #{inspect(value)}"} + end + end + def validate(%Schema{enum: options = [_ | _]}, value, path, _schemas) do + case Enum.member?(options, value) do + true -> :ok + _ -> + {:error, "#{path}: Value not in enum: #{inspect(value)}"} + end + end + def validate(%Schema{type: type}, value, path, _schemas) when type in [:integer, :number] and not is_number(value) do + {:error, "#{path}: expected value of type #{type}"} end def validate(schema = %Schema{type: type}, value, path, _schemas) when type in [:integer, :number] do with :ok <- validate_multiple(schema, value, path), @@ -442,8 +475,7 @@ defmodule OpenApiSpex.Schema do def validate(schema = %Schema{type: :string}, value, path, _schemas) do with :ok <- validate_max_length(schema, value, path), :ok <- validate_min_length(schema, value, path), - :ok <- validate_pattern(schema, value, path), - :ok <- validate_enum(schema, value, path) do + :ok <- validate_pattern(schema, value, path) do :ok end end @@ -470,6 +502,15 @@ defmodule OpenApiSpex.Schema do :ok end end + # Note: OpenAPI3's `{"nullable": true}` really means JSON schema's `{}` (i.e. anything) + def validate(%Schema{nullable: true}, nil, _path, _schemas), do: :ok + def validate(%Schema{nullable: _}, nil, path, _schemas) do + {:error, "#{path}: unexpected null value"} + end + def validate(%Schema{nullable: true}, _value, _path, _schemas), do: :ok + def validate(_schema, value, path, _schemas) do + {:error, "#{path}: Invalid value: #{inspect(value)}"} + end @spec validate_multiple(Schema.t, number, String.t) :: :ok | {:error, String.t} defp validate_multiple(%{multipleOf: nil}, _, _), do: :ok @@ -516,17 +557,7 @@ defmodule OpenApiSpex.Schema do defp validate_pattern(%{pattern: regex = %Regex{}}, val, path) do case Regex.match?(regex, val) do true -> :ok - _ -> {:error, "#{path}: Value does not match pattern: #{regex.source}"} - end - end - - @spec validate_enum(Schema.t, String.t, String.t) :: :ok | {:error, String.t} - def validate_enum(%{enum: nil}, _val, _path), do: :ok - def validate_enum(%{enum: options}, value, path) do - case Enum.member?(options, value) do - true -> :ok - _ -> - {:error, "#{path}: Value not in enum: #{Enum.join(options, ", ")}"} + _ -> {:error, "#{path}: Value #{inspect(val)} does not match pattern: #{regex.source}"} end end diff --git a/test/open_api_spex_test.exs b/test/open_api_spex_test.exs index 46075109..14edc62c 100644 --- a/test/open_api_spex_test.exs +++ b/test/open_api_spex_test.exs @@ -61,7 +61,7 @@ defmodule OpenApiSpexTest do conn = OpenApiSpexTest.Router.call(conn, []) assert conn.status == 422 - assert conn.resp_body == "#/user/name: Value does not match pattern: [a-zA-Z][a-zA-Z0-9_]+" + assert conn.resp_body == "#/user/name: Value \"*1234\" does not match pattern: [a-zA-Z][a-zA-Z0-9_]+" end end -end \ No newline at end of file +end diff --git a/test/schema_test.exs b/test/schema_test.exs index f494989d..50df17bf 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -138,4 +138,120 @@ defmodule OpenApiSpex.SchemaTest do assert :ok = Schema.validate(schema, "bar", %{}) end + test "Validate enum with expected value" do + schema = %Schema{ + enum: ["foo", %{id: 42}] + } + assert :ok = Schema.validate(schema, %{id: 42}, %{}) + end + + test "Validate enum with unexpected value" do + schema = %Schema{ + enum: ["foo", %{id: 42}] + } + assert {:error, _} = Schema.validate(schema, "baz", %{}) + end + + test "Validate non-empty string with expected value" do + schema = %Schema{type: :string, minLength: 1} + assert :ok = Schema.validate(schema, "BLIP", %{}) + end + + test "Validate nullable-ified with expected value" do + schema = %Schema{ + allOf: [ + %Schema{nullable: true}, + %Schema{type: :string, minLength: 1} + ]} + assert :ok = Schema.validate(schema, "BLIP", %{}) + end + + test "Validate nullable with expected value" do + schema = %Schema{nullable: true} + assert :ok = Schema.validate(schema, nil, %{}) + end + + test "Validate nullable with unexpected value" do + schema = %Schema{nullable: true} + assert :ok = Schema.validate(schema, "bla", %{}) + end + + test "Validate oneOf with expected values" do + schema = %Schema{ + oneOf: [%Schema{nullable: true}, %Schema{type: :integer}] + } + assert {:error, _} = Schema.validate(schema, 42, %{}) + assert :ok = Schema.validate(schema, nil, %{}) + end + + test "Validate oneOf with value matching no schema" do + schema = %Schema{ + oneOf: [%Schema{nullable: true}, %Schema{type: :integer}] + } + assert :ok = Schema.validate(schema, "bla", %{}) + end + + test "Validate oneOf with value matching more than one schema" do + schema = %Schema{ + oneOf: [%Schema{type: :number}, %Schema{type: :integer}] + } + assert {:error, _} = Schema.validate(schema, 42, %{}) + end + + test "Validate anyOf with expected values" do + schema = %Schema{ + anyOf: [%Schema{nullable: true}, %Schema{type: :integer}] + } + assert :ok = Schema.validate(schema, 42, %{}) + assert :ok = Schema.validate(schema, nil, %{}) + end + + test "Validate anyOf with value matching no schema" do + schema = %Schema{ + anyOf: [%Schema{nullable: true}, %Schema{type: :integer}] + } + assert :ok = Schema.validate(schema, "bla", %{}) + end + + test "Validate anyOf with value matching more than one schema" do + schema = %Schema{ + anyOf: [%Schema{type: :number}, %Schema{type: :integer}] + } + assert :ok = Schema.validate(schema, 42, %{}) + end + + test "Validate allOf with expected values" do + schema = %Schema{ + allOf: [%Schema{type: :number}, %Schema{type: :integer}] + } + assert :ok = Schema.validate(schema, 42, %{}) + end + + test "Validate allOf with value matching no schema" do + schema = %Schema{ + allOf: [%Schema{nullable: true}, %Schema{type: :integer}] + } + assert {:error, _} = Schema.validate(schema, "bla", %{}) + end + + test "Validate allOf with value matching not all schemas" do + schema = %Schema{ + allOf: [%Schema{nullable: true}, %Schema{type: :integer}] + } + assert :ok = Schema.validate(schema, 42, %{}) + assert {:error, _} = Schema.validate(schema, nil, %{}) + end + + test "Verify 'not' validation" do + schema = %Schema{not: %Schema{type: :boolean}} + assert :ok = Schema.validate(schema, 42, %{}) + assert :ok = Schema.validate(schema, "42", %{}) + assert :ok = Schema.validate(schema, nil, %{}) + assert :ok = Schema.validate(schema, 4.2, %{}) + assert :ok = Schema.validate(schema, [4], %{}) + assert :ok = Schema.validate(schema, %{}, %{}) + assert {:error, _} = Schema.validate(schema, true, %{}) + assert {:error, _} = Schema.validate(schema, false, %{}) + end + end