From f91bf7b771d91d949a7f188620818adf13e55a34 Mon Sep 17 00:00:00 2001 From: Michael Buhot Date: Sun, 28 Oct 2018 18:32:36 +1000 Subject: [PATCH] Incorporate improvements from #42 - Validate enums of non-string types - Validate anyOf/oneOf/allOf/not before type testing - Improve error message for regex validations --- lib/open_api_spex/schema.ex | 90 ++++----- test/open_api_spex_test.exs | 2 +- test/schema_test.exs | 393 +++++++++++++++++++++++------------- 3 files changed, 297 insertions(+), 188 deletions(-) diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex index 98fa9ba4..8a397c8a 100644 --- a/lib/open_api_spex/schema.ex +++ b/lib/open_api_spex/schema.ex @@ -415,7 +415,7 @@ defmodule OpenApiSpex.Schema do end end - @doc """ + @doc ~S""" Validate a value against a Schema. This expects that the value has already been `cast` to the appropriate data type. @@ -429,7 +429,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) @@ -437,9 +437,46 @@ 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{nullable: true}, nil, _path, _schemas), do: :ok - def validate(%Schema{type: type}, nil, path, _schemas) do + def validate(%Schema{type: type}, nil, path, _schemas) when not is_nil(type) do {:error, "#{path}: null value where #{type} expected"} end + def validate(schema = %Schema{anyOf: valid_schemas}, value, path, schemas) when is_list(valid_schemas) do + if Enum.any?(valid_schemas, &validate(&1, value, path, schemas) == :ok) do + validate(%{schema | anyOf: nil}, value, path, schemas) + else + {:error, "#{path}: Failed to validate against any schema"} + end + end + def validate(schema = %Schema{oneOf: valid_schemas}, value, path, schemas) when is_list(valid_schemas) do + case Enum.count(valid_schemas, &validate(&1, value, path, schemas) == :ok) do + 1 -> validate(%{schema | oneOf: nil}, value, path, schemas) + 0 -> {:error, "#{path}: Failed to validate against any schema"} + other -> {:error, "#{path}: Validated against #{other} schemas when only one expected"} + end + end + def validate(schema = %Schema{allOf: required_schemas}, value, path, schemas) when is_list(required_schemas) do + required_schemas + |> Enum.map(&validate(&1, value, path, schemas)) + |> Enum.reject(& &1 == :ok) + |> Enum.map(fn {:error, msg} -> msg end) + |> case do + [] -> validate(%{schema | allOf: nil}, value, path, schemas) + errors -> {:error, Enum.join(errors, "\n")} + end + end + def validate(schema = %Schema{not: not_schema}, value, path, schemas) when not is_nil(not_schema) do + case validate(not_schema, value, path, schemas) do + {:error, _} -> validate(%{schema | not: nil}, value, path, schemas) + :ok -> {:error, "#{path}: Value is valid for schema given in `not`"} + 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 = %Schema{type: :integer}, value, path, _schemas) when is_integer(value) do validate_number_types(schema, value, path) end @@ -480,41 +517,11 @@ defmodule OpenApiSpex.Schema do :ok end end - def validate(schema = %Schema{anyOf: valid_schemas}, value, path, schemas) when is_list(valid_schemas) do - if Enum.any?(valid_schemas, &validate(&1, value, path, schemas) == :ok) do - validate(%{schema | anyOf: nil}, value, path, schemas) - else - {:error, "#{path}: Failed to validate against any schema"} - end - end - def validate(schema = %Schema{oneOf: valid_schemas}, value, path, schemas) when is_list(valid_schemas) do - case Enum.count(valid_schemas, &validate(&1, value, path, schemas) == :ok) do - 1 -> validate(%{schema | oneOf: nil}, value, path, schemas) - 0 -> {:error, "#{path}: Failed to validate against any schema"} - other -> {:error, "#{path}: Validated against #{other} schemas when only one expected"} - end - end - def validate(schema = %Schema{allOf: required_schemas}, value, path, schemas) when is_list(required_schemas) do - required_schemas - |> Enum.map(&validate(&1, value, path, schemas)) - |> Enum.reject(& &1 == :ok) - |> Enum.map(fn {:error, msg} -> msg end) - |> case do - [] -> validate(%{schema | allOf: nil}, value, path, schemas) - errors -> {:error, Enum.join(errors, "\n")} - end - end - def validate(schema = %Schema{not: not_schema}, value, path, schemas) when not is_nil(not_schema) do - case validate(not_schema, value, path, schemas) do - {:error, _} -> validate(%{schema | not: nil}, value, path, schemas) - :ok -> {:error, "#{path}: Value is valid for schema given in `not`"} - end - end def validate(%Schema{type: nil}, _value, _path, _schemas) do # polymorphic schemas will terminate here after validating against anyOf/oneOf/allOf/not :ok end - def validate(%Schema{type: expected_type}, value, path, _schemas) do + def validate(%Schema{type: expected_type}, value, path, _schemas) when not is_nil(expected_type) do {:error, "#{path}: invalid type #{term_type(value)} where #{expected_type} expected"} end @@ -542,8 +549,7 @@ defmodule OpenApiSpex.Schema do defp validate_string_types(schema, value, path) 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 @@ -593,17 +599,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 0ccdb0f1..e2701bfa 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 diff --git a/test/schema_test.exs b/test/schema_test.exs index 17a256cc..1bbb7a5b 100644 --- a/test/schema_test.exs +++ b/test/schema_test.exs @@ -110,6 +110,7 @@ defmodule OpenApiSpex.SchemaTest do %Schema{type: :string, format: :"date-time"} ] } + assert {:ok, %DateTime{}} = Schema.cast(schema, "2018-04-01T12:34:56Z", %{}) end @@ -120,181 +121,293 @@ defmodule OpenApiSpex.SchemaTest do %Schema{type: :string, format: :"date-time"} ] } + assert {:ok, %DateTime{}} = Schema.cast(schema, "2018-04-01T12:34:56Z", %{}) end - test "Validate string with unexpected value" do - schema = %Schema{ - type: :string, - enum: ["foo", "bar"] - } - assert {:error, _} = Schema.validate(schema, "baz", %{}) - end + describe "Integer validation" do + test "Validate schema type integer when value is object" do + schema = %Schema{ + type: :integer + } - test "Validate string with expected value" do - schema = %Schema{ - type: :string, - enum: ["foo", "bar"] - } - assert :ok = Schema.validate(schema, "bar", %{}) + assert {:error, _} = Schema.validate(schema, %{}, %{}) + end end - test "Validate schema type object when value is array" do - schema = %Schema{ - type: :object - } - assert {:error, _} = Schema.validate(schema, [], %{}) - end + describe "Number validation" do + test "Validate schema type number when value is object" do + schema = %Schema{ + type: :integer + } - test "Validate schema type array when value is object" do - schema = %Schema{ - type: :array - } - assert {:error, _} = Schema.validate(schema, %{}, %{}) + assert {:error, _} = Schema.validate(schema, %{}, %{}) + end end - test "Validate schema type boolean when value is object" do - schema = %Schema{ - type: :boolean - } - assert {:error, _} = Schema.validate(schema, %{}, %{}) - end + describe "String validation" do + test "Validate schema type string when value is object" do + schema = %Schema{ + type: :string + } + assert {:error, _} = Schema.validate(schema, %{}, %{}) + end - test "Validate schema type string when value is object" do - schema = %Schema{ - type: :string - } - assert {:error, _} = Schema.validate(schema, %{}, %{}) - end + test "Validate schema type string when value is DateTime" do + schema = %Schema{ + type: :string + } + assert {:error, _} = Schema.validate(schema, DateTime.utc_now(), %{}) + end - test "Validate schema type string when value is DateTime" do - schema = %Schema{ - type: :string - } - assert {:error, _} = Schema.validate(schema, DateTime.utc_now(), %{}) + test "Validate non-empty string with expected value" do + schema = %Schema{type: :string, minLength: 1} + assert :ok = Schema.validate(schema, "BLIP", %{}) + end end - test "Validate schema type object when value is DateTime" do - schema = %Schema{ - type: :object - } - assert {:error, _} = Schema.validate(schema, DateTime.utc_now(), %{}) - end + describe "DateTime validation" do + test "Validate schema type string with format date-time when value is DateTime" do + schema = %Schema{ + type: :string, + format: :"date-time" + } - test "Validate schema type string with format date-time when value is DateTime" do - schema = %Schema{ - type: :string, - format: :"date-time" - } - assert :ok = Schema.validate(schema, DateTime.utc_now(), %{}) + assert :ok = Schema.validate(schema, DateTime.utc_now(), %{}) + end end - test "Validate schema type string with format date when value is Date" do - schema = %Schema{ - type: :string, - format: :date - } - assert :ok = Schema.validate(schema, Date.utc_today(), %{}) - end + describe "Date Validation" do + test "Validate schema type string with format date when value is Date" do + schema = %Schema{ + type: :string, + format: :date + } - test "Validate schema type integer when value is object" do - schema = %Schema{ - type: :integer - } - assert {:error, _} = Schema.validate(schema, %{}, %{}) + assert :ok = Schema.validate(schema, Date.utc_today(), %{}) + end end - test "Validate schema type number when value is object" do - schema = %Schema{ - type: :integer - } - assert {:error, _} = Schema.validate(schema, %{}, %{}) - end + describe "Enum validation" do + test "Validate string enum with unexpected value" do + schema = %Schema{ + type: :string, + enum: ["foo", "bar"] + } - test "Validate anyOf schema with valid value" do - schema = %Schema { - anyOf: [ - %Schema{type: :array}, - %Schema{type: :string} - ] - } - assert :ok = Schema.validate(schema, "a string", %{}) - end + assert {:error, _} = Schema.validate(schema, "baz", %{}) + end - test "Validate anyOf schema with invalid value" do - schema = %Schema { - anyOf: [ - %Schema{type: :string}, - %Schema{type: :array} - ] - } - assert {:error, _} = Schema.validate(schema, 3.14159, %{}) + test "Validate string enum with expected value" do + schema = %Schema{ + type: :string, + enum: ["foo", "bar"] + } + + assert :ok = Schema.validate(schema, "bar", %{}) + end end - test "Validate oneOf schema with valid value" do - schema = %Schema { - oneOf: [ - %Schema{type: :string}, - %Schema{type: :array} - ] - } - assert :ok = Schema.validate(schema, [1,2,3], %{}) + describe "Object validation" do + test "Validate schema type object when value is array" do + schema = %Schema{ + type: :object + } + + assert {:error, _} = Schema.validate(schema, [], %{}) + end + + test "Validate schema type object when value is DateTime" do + schema = %Schema{ + type: :object + } + + assert {:error, _} = Schema.validate(schema, DateTime.utc_now(), %{}) + end end - test "Validate oneOf schema with invalid value" do - schema = %Schema { - oneOf: [ - %Schema{type: :string}, - %Schema{type: :array} - ] - } - assert {:error, _} = Schema.validate(schema, 3.14159, %{}) + describe "Array validation" do + test "Validate schema type array when value is object" do + schema = %Schema{ + type: :array + } + + assert {:error, _} = Schema.validate(schema, %{}, %{}) + end end - test "Validate oneOf schema when matching multiple schemas" do - schema = %Schema { - oneOf: [ - %Schema{type: :object, properties: %{a: %Schema{type: :string}}}, - %Schema{type: :object, properties: %{b: %Schema{type: :string}}} - ] - } - assert {:error, _} = Schema.validate(schema, %{a: "a", b: "b"}, %{}) + describe "Boolean validation" do + test "Validate schema type boolean when value is object" do + schema = %Schema{ + type: :boolean + } + + assert {:error, _} = Schema.validate(schema, %{}, %{}) + end end - test "Validate allOf schema with valid value" do - schema = %Schema { - allOf: [ - %Schema{type: :object, properties: %{a: %Schema{type: :string}}}, - %Schema{type: :object, properties: %{b: %Schema{type: :string}}} - ] - } - assert :ok = Schema.validate(schema, %{a: "a", b: "b"}, %{}) + describe "AnyOf validation" do + test "Validate anyOf schema with valid value" do + schema = %Schema{ + anyOf: [ + %Schema{type: :array}, + %Schema{type: :string} + ] + } + + assert :ok = Schema.validate(schema, "a string", %{}) + 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 anyOf schema with invalid value" do + schema = %Schema{ + anyOf: [ + %Schema{type: :string}, + %Schema{type: :array} + ] + } + + assert {:error, _} = Schema.validate(schema, 3.14159, %{}) + end end - test "Validate allOf schema with invalid value" do - schema = %Schema { - allOf: [ - %Schema{type: :object, properties: %{a: %Schema{type: :string}}}, - %Schema{type: :object, properties: %{b: %Schema{type: :string}}} - ] - } - assert {:error, msg} = Schema.validate(schema, %{a: 1, b: 2}, %{}) - assert msg =~ "#/a" - assert msg =~ "#/b" + describe "OneOf validation" do + test "Validate oneOf schema with valid value" do + schema = %Schema{ + oneOf: [ + %Schema{type: :string}, + %Schema{type: :array} + ] + } + + assert :ok = Schema.validate(schema, [1, 2, 3], %{}) + end + + test "Validate oneOf schema with invalid value" do + schema = %Schema{ + oneOf: [ + %Schema{type: :string}, + %Schema{type: :array} + ] + } + + assert {:error, _} = Schema.validate(schema, 3.14159, %{}) + end + + test "Validate oneOf schema when matching multiple schemas" do + schema = %Schema{ + oneOf: [ + %Schema{type: :object, properties: %{a: %Schema{type: :string}}}, + %Schema{type: :object, properties: %{b: %Schema{type: :string}}} + ] + } + + assert {:error, _} = Schema.validate(schema, %{a: "a", b: "b"}, %{}) + end end - test "Validate not schema with valid value" do - schema = %Schema { - not: %Schema{type: :object} - } - assert :ok = Schema.validate(schema, 1, %{}) + describe "AllOf validation" do + test "Validate allOf schema with valid value" do + schema = %Schema{ + allOf: [ + %Schema{type: :object, properties: %{a: %Schema{type: :string}}}, + %Schema{type: :object, properties: %{b: %Schema{type: :string}}} + ] + } + + assert :ok = Schema.validate(schema, %{a: "a", b: "b"}, %{}) + end + + test "Validate allOf schema with invalid value" do + schema = %Schema{ + allOf: [ + %Schema{type: :object, properties: %{a: %Schema{type: :string}}}, + %Schema{type: :object, properties: %{b: %Schema{type: :string}}} + ] + } + + assert {:error, msg} = Schema.validate(schema, %{a: 1, b: 2}, %{}) + assert msg =~ "#/a" + assert msg =~ "#/b" + end + + test "Validate allOf with value matching not all schemas" do + schema = %Schema{ + allOf: [ + %Schema{ + type: :integer, + minimum: 5 + }, + %Schema{ + type: :integer, + maximum: 40 + } + ] + } + + assert {:error, _} = Schema.validate(schema, 42, %{}) + end end - test "Validate not schema with invalid value" do - schema = %Schema { - not: %Schema{type: :object} - } - assert {:error, _} = Schema.validate(schema, %{a: 1}, %{}) + describe "Not validation" do + test "Validate not schema with valid value" do + schema = %Schema{ + not: %Schema{type: :object} + } + + assert :ok = Schema.validate(schema, 1, %{}) + end + + test "Validate not schema with invalid value" do + schema = %Schema{ + not: %Schema{type: :object} + } + + assert {:error, _} = Schema.validate(schema, %{a: 1}, %{}) + 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 + describe "Nullable validation" do + test "Validate nullable-ified with expected value" do + schema = %Schema{ + nullable: true, + type: :string, + minLength: 1 + } + + assert :ok = Schema.validate(schema, "BLIP", %{}) + end + + test "Validate nullable with expected value" do + schema = %Schema{type: :string, nullable: true} + assert :ok = Schema.validate(schema, nil, %{}) + end + + test "Validate nullable with unexpected value" do + schema = %Schema{type: :string, nullable: true} + assert :ok = Schema.validate(schema, "bla", %{}) + end + end end