From fa219537f38a85cb6f1bb3766b61896bcfa85beb Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 27 Aug 2018 18:45:31 +0200 Subject: [PATCH 1/9] validate oneOf schemas --- lib/open_api_spex/schema.ex | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index d8959a15..cdb6dd51 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -428,6 +428,13 @@ defmodule OpenApiSpex.Schema do @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{oneOf: schemasOf = [_|_]}, value, path, schemas) do + case Enum.count(schemasOf, fn schema -> try_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{nullable: true}, nil, _path, _schemas), do: :ok def validate(%Schema{type: type}, nil, path, _schemas) do {:error, "#{path}: null value where #{type} expected"} @@ -471,6 +478,16 @@ defmodule OpenApiSpex.Schema do end end + defp try_validate?(schema, value, path, schemas) do + try do + :ok == validate(schema, value, path, schemas) + rescue + FunctionClauseError -> + # validate/4 can fail this way on invalid values + false + end + end + @spec validate_multiple(Schema.t, number, String.t) :: :ok | {:error, String.t} defp validate_multiple(%{multipleOf: nil}, _, _), do: :ok defp validate_multiple(%{multipleOf: n}, value, _) when (round(value / n) * n == value), do: :ok From 72f940d06e9d4ab8f41062a0f0f2b288e6c09404 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 27 Aug 2018 18:46:10 +0200 Subject: [PATCH 2/9] add some little unit tests --- test/schema_test.exs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/schema_test.exs b/test/schema_test.exs index f494989d..5f02d58b 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -138,4 +138,36 @@ defmodule OpenApiSpex.SchemaTest do assert :ok = Schema.validate(schema, "bar", %{}) 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_raise FunctionClauseError, fn -> Schema.validate(schema, "bla", %{}) end + end + + test "Validate oneOf with expected values" do + schema = %Schema{ + oneOf: [%Schema{nullable: true}, %Schema{type: :integer}] + } + assert :ok = 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 {:error, _} = 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 + end From ed75a272c6a538f34cd96c5cf7ffa9ce2c4f887b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 27 Aug 2018 18:47:46 +0200 Subject: [PATCH 3/9] fix some wrong validation code --- lib/open_api_spex/schema.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index cdb6dd51..0fbdfe96 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -436,8 +436,11 @@ defmodule OpenApiSpex.Schema do end end 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{nullable: _}, nil, path, _schemas) do + {:error, "#{path}: unexpected null value"} + 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), From 528675e835fc8a984bcfdea4c340c711f70f7cbf Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 27 Aug 2018 18:54:38 +0200 Subject: [PATCH 4/9] also do anyOf and allOf --- lib/open_api_spex/schema.ex | 14 ++++++++++++ test/schema_test.exs | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index 0fbdfe96..94ba8b69 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -435,6 +435,20 @@ defmodule OpenApiSpex.Schema do _ -> {: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 -> try_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 -> try_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{nullable: true}, nil, _path, _schemas), do: :ok def validate(%Schema{nullable: _}, nil, path, _schemas) do {:error, "#{path}: unexpected null value"} diff --git a/test/schema_test.exs b/test/schema_test.exs index 5f02d58b..d5ec4e41 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -170,4 +170,48 @@ defmodule OpenApiSpex.SchemaTest do 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 {:error, _} = 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 {:error, _} = Schema.validate(schema, 42, %{}) + assert {:error, _} = Schema.validate(schema, nil, %{}) + end + end From c6c62c1a8f6fc68c1e640b4fc6090456df144656 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Mon, 27 Aug 2018 19:02:45 +0200 Subject: [PATCH 5/9] handle not too --- lib/open_api_spex/schema.ex | 6 ++++++ test/schema_test.exs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index 94ba8b69..bf95f62d 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -449,6 +449,12 @@ defmodule OpenApiSpex.Schema do {: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{nullable: true}, nil, _path, _schemas), do: :ok def validate(%Schema{nullable: _}, nil, path, _schemas) do {:error, "#{path}: unexpected null value"} diff --git a/test/schema_test.exs b/test/schema_test.exs index d5ec4e41..da571075 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -214,4 +214,16 @@ defmodule OpenApiSpex.SchemaTest do 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 From e8404295a5b2ca064920e2ef66f4e09f9a740272 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 28 Aug 2018 10:49:14 +0200 Subject: [PATCH 6/9] never unexpectedly raise FunctionClauseError in validate/4 --- lib/open_api_spex/schema.ex | 17 +++++------------ test/schema_test.exs | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index bf95f62d..7b33ea0e 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -429,21 +429,21 @@ defmodule OpenApiSpex.Schema do @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{oneOf: schemasOf = [_|_]}, value, path, schemas) do - case Enum.count(schemasOf, fn schema -> try_validate?(schema, value, path, schemas) end) 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 -> try_validate?(schema, value, path, schemas) end) 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 -> try_validate?(schema, value, path, schemas) end) 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)}"} @@ -500,15 +500,8 @@ defmodule OpenApiSpex.Schema do :ok end end - - defp try_validate?(schema, value, path, schemas) do - try do - :ok == validate(schema, value, path, schemas) - rescue - FunctionClauseError -> - # validate/4 can fail this way on invalid values - false - end + 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} diff --git a/test/schema_test.exs b/test/schema_test.exs index da571075..2f9d3863 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -145,7 +145,7 @@ defmodule OpenApiSpex.SchemaTest do test "Validate nullable with unexpected value" do schema = %Schema{nullable: true} - assert_raise FunctionClauseError, fn -> Schema.validate(schema, "bla", %{}) end + assert {:error, _} = Schema.validate(schema, "bla", %{}) end test "Validate oneOf with expected values" do From a064054de173e9b116833d5b6935b5c0d36149eb Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 28 Aug 2018 11:08:50 +0200 Subject: [PATCH 7/9] fix enum validation --- lib/open_api_spex/schema.ex | 20 ++++++++------------ test/schema_test.exs | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index 7b33ea0e..cd42cdab 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -459,6 +459,13 @@ defmodule OpenApiSpex.Schema do def validate(%Schema{nullable: _}, nil, path, _schemas) do {:error, "#{path}: unexpected null value"} 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 @@ -472,8 +479,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 @@ -553,16 +559,6 @@ defmodule OpenApiSpex.Schema do 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, ", ")}"} - end - end - @spec validate_max_items(Schema.t, list, String.t) :: :ok | {:error, String.t} defp validate_max_items(%Schema{maxItems: nil}, _val, _path), do: :ok defp validate_max_items(%Schema{maxItems: n}, value, _path) when length(value) <= n, do: :ok diff --git a/test/schema_test.exs b/test/schema_test.exs index 2f9d3863..1c654ac0 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -138,6 +138,20 @@ 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 nullable with expected value" do schema = %Schema{nullable: true} assert :ok = Schema.validate(schema, nil, %{}) From b477928f6dc6ff52260edaf8d79387961d78b62b Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Tue, 28 Aug 2018 12:33:28 +0200 Subject: [PATCH 8/9] show value on failing pattern --- lib/open_api_spex/schema.ex | 4 ++-- test/open_api_spex_test.exs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index cd42cdab..211b6c73 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -421,7 +421,7 @@ 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) @@ -555,7 +555,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}"} + _ -> {: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 From e8162408449157487fe8de58bf421d354a6e59c0 Mon Sep 17 00:00:00 2001 From: Pierre Fenoll Date: Thu, 30 Aug 2018 16:15:48 +0200 Subject: [PATCH 9/9] really fix handling of nullable --- lib/open_api_spex/schema.ex | 10 ++++++---- test/schema_test.exs | 24 +++++++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index 211b6c73..deb9d177 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -455,10 +455,6 @@ defmodule OpenApiSpex.Schema do :ok -> {:error, "#{path}: Schema should \"not\" be matching: #{inspect(value)}"} end end - 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{enum: options = [_ | _]}, value, path, _schemas) do case Enum.member?(options, value) do true -> :ok @@ -506,6 +502,12 @@ 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 diff --git a/test/schema_test.exs b/test/schema_test.exs index 1c654ac0..50df17bf 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -152,6 +152,20 @@ defmodule OpenApiSpex.SchemaTest do 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, %{}) @@ -159,14 +173,14 @@ defmodule OpenApiSpex.SchemaTest do test "Validate nullable with unexpected value" do schema = %Schema{nullable: true} - assert {:error, _} = Schema.validate(schema, "bla", %{}) + assert :ok = Schema.validate(schema, "bla", %{}) end test "Validate oneOf with expected values" do schema = %Schema{ oneOf: [%Schema{nullable: true}, %Schema{type: :integer}] } - assert :ok = Schema.validate(schema, 42, %{}) + assert {:error, _} = Schema.validate(schema, 42, %{}) assert :ok = Schema.validate(schema, nil, %{}) end @@ -174,7 +188,7 @@ defmodule OpenApiSpex.SchemaTest do schema = %Schema{ oneOf: [%Schema{nullable: true}, %Schema{type: :integer}] } - assert {:error, _} = Schema.validate(schema, "bla", %{}) + assert :ok = Schema.validate(schema, "bla", %{}) end test "Validate oneOf with value matching more than one schema" do @@ -196,7 +210,7 @@ defmodule OpenApiSpex.SchemaTest do schema = %Schema{ anyOf: [%Schema{nullable: true}, %Schema{type: :integer}] } - assert {:error, _} = Schema.validate(schema, "bla", %{}) + assert :ok = Schema.validate(schema, "bla", %{}) end test "Validate anyOf with value matching more than one schema" do @@ -224,7 +238,7 @@ defmodule OpenApiSpex.SchemaTest do schema = %Schema{ allOf: [%Schema{nullable: true}, %Schema{type: :integer}] } - assert {:error, _} = Schema.validate(schema, 42, %{}) + assert :ok = Schema.validate(schema, 42, %{}) assert {:error, _} = Schema.validate(schema, nil, %{}) end