Page MenuHomePhorge

No OneTemporary

Size
7 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/cast.ex b/lib/open_api_spex/cast.ex
index 34a44cd..cbaa55e 100644
--- a/lib/open_api_spex/cast.ex
+++ b/lib/open_api_spex/cast.ex
@@ -1,55 +1,57 @@
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}
cast(ctx)
end
+ # nil schema
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
- # nullable
+ # nullable: true
def cast(%CastContext{value: nil, schema: %{nullable: true}}) do
{:ok, nil}
end
- # nullable
+ # nullable: false
def cast(%CastContext{value: nil} = ctx) do
CastContext.error(ctx, {:null_value})
end
# Enum
def cast(%CastContext{schema: %{enum: []}} = ctx) do
cast(%{ctx | enum: nil})
end
+ # Enum
def cast(%CastContext{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
CastContext.error(ctx, {:invalid_enum})
end
end
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 181c5e3..c5fc352 100644
--- a/lib/open_api_spex/cast_error.ex
+++ b/lib/open_api_spex/cast_error.ex
@@ -1,116 +1,126 @@
defmodule OpenApiSpex.CastError do
alias OpenApiSpex.TermType
defstruct reason: nil,
value: nil,
format: nil,
type: nil,
name: nil,
- path: []
+ path: [],
+ length: 0
def new(ctx, {:null_value}) do
type = ctx.schema && ctx.schema.type
%__MODULE__{reason: :null_value, type: type}
|> add_context_fields(ctx)
end
+ def new(ctx, {:min_length, length}) do
+ %__MODULE__{reason: :min_length, length: length}
+ |> 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, {:invalid_enum}) do
%__MODULE__{reason: :invalid_enum}
|> 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} = error) do
message =
case error.type do
nil -> "null value"
type -> "null value where #{type} expected"
end
message
end
+ def message(%{reason: :min_length, length: length}) do
+ "String length is smaller than minLength: #{length}"
+ end
+
def message(%{reason: :invalid_type, type: type, value: value}) do
"Invalid #{type}. Got: #{TermType.type(value)}"
end
def message(%{reason: :invalid_format, format: format}) do
"Invalid format. Expected #{inspect(format)}"
end
def message(%{reason: :invalid_enum}) do
"Invalid value for enum"
end
def message(%{reason: :polymorphic_failed, type: polymorphic_type}) do
"Failed to cast to any schema in #{polymorphic_type}"
end
def message(%{reason: :unexpected_field, name: name}) do
"Unexpected field: #{safe_string(name)}"
end
def message(%{reason: :no_value_required_for_discriminator, name: field}) do
"No value for required disciminator property: #{field}"
end
def message(%{reason: :unknown_schema, name: name}) do
"Unknown schema: #{name}"
end
def message(%{reason: :missing_field, name: name}) do
"Missing field: #{name}"
end
def message_with_path(error) do
prepend_path(error, message(error))
end
def path_to_string(%{path: path} = _error) do
"/" <> (path |> Enum.map(&to_string/1) |> Path.join())
end
defp add_context_fields(error, ctx) do
%{error | path: Enum.reverse(ctx.path), value: ctx.value}
end
defp prepend_path(error, message) do
path =
case error.path do
[] -> "#"
_ -> "#" <> path_to_string(error)
end
path <> ": " <> message
end
defp safe_string(string) do
to_string(string) |> String.slice(0..39)
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_string.ex b/lib/open_api_spex/cast_string.ex
index 5ab2a63..e67cbbb 100644
--- a/lib/open_api_spex/cast_string.ex
+++ b/lib/open_api_spex/cast_string.ex
@@ -1,21 +1,41 @@
defmodule OpenApiSpex.CastString do
@moduledoc false
alias OpenApiSpex.CastContext
def cast(%{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
- def cast(%{value: value}) when is_binary(value) do
- {:ok, value}
+ def cast(%{value: value} = ctx) when is_binary(value) do
+ cast_binary(ctx)
end
def cast(ctx) do
CastContext.error(ctx, {:invalid_type, :string})
end
+
+ ## Private functions
+
+ defp cast_binary(%{value: value, schema: %{minLength: min_length}} = ctx)
+ when is_integer(min_length) do
+ # Note: This is not part of the JSON Shema spec: trim string before measuring length
+ # It's just too important to miss
+ trimmed = String.trim(value)
+ length = String.length(trimmed)
+
+ if length < min_length do
+ CastContext.error(ctx, {:min_length, length})
+ else
+ {:ok, value}
+ end
+ end
+
+ defp cast_binary(%{value: value}) do
+ {:ok, value}
+ end
end
diff --git a/test/cast_string_test.exs b/test/cast_string_test.exs
index 082d2a4..abb2584 100644
--- a/test/cast_string_test.exs
+++ b/test/cast_string_test.exs
@@ -1,26 +1,34 @@
defmodule OpenApiSpex.CastStringTest do
use ExUnit.Case
alias OpenApiSpex.{CastContext, CastError, CastString, Schema}
defp cast(ctx), do: CastString.cast(struct(CastContext, ctx))
describe "cast/1" do
test "basics" 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
+
+ # Note: we measure length of string after trimming leading and trailing whitespace
+ test "minLength" do
+ schema = %Schema{type: :string, minLength: 1}
+ assert {:error, [error]} = cast(value: " ", schema: schema)
+ assert %CastError{} = error
+ assert error.reason == :min_length
+ end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 30, 2:36 PM (1 d, 18 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41454
Default Alt Text
(7 KB)

Event Timeline