Page MenuHomePhorge

No OneTemporary

Size
19 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index bc0aae6..3e4522c 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,135 +1,139 @@
defmodule OpenApiSpex do
@moduledoc """
Provides the entry-points for defining schemas, validating and casting.
"""
- alias OpenApiSpex.{OpenApi, Operation, Operation2, Reference, Schema, SchemaResolver}
+ alias OpenApiSpex.{CastError, OpenApi, Operation, Operation2, Reference, Schema, SchemaResolver}
@doc """
Adds schemas to the api spec from the modules specified in the Operations.
Eg, if the response schema for an operation is defined with:
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
Then the `UserResponse.schema()` function will be called to load the schema, and
a `Reference` to the loaded schema will be used in the operation response.
See `OpenApiSpex.schema` macro for a convenient syntax for defining schema modules.
"""
@spec resolve_schema_modules(OpenApi.t) :: OpenApi.t
def resolve_schema_modules(spec = %OpenApi{}) do
SchemaResolver.resolve_schema_modules(spec)
end
def cast_and_validate(
spec = %OpenApi{},
operation = %Operation{},
conn = %Plug.Conn{},
content_type \\ nil
) do
Operation2.cast(operation, conn, content_type, spec.components.schemas)
end
@doc """
Cast params to conform to a `OpenApiSpex.Schema`.
See `OpenApiSpex.Schema.cast/3` for additional examples and details.
"""
@spec cast(OpenApi.t, Schema.t | Reference.t, any) :: {:ok, any} | {:error, String.t}
def cast(spec = %OpenApi{}, schema = %Schema{}, params) do
Schema.cast(schema, params, spec.components.schemas)
end
def cast(spec = %OpenApi{}, schema = %Reference{}, params) do
Schema.cast(schema, params, spec.components.schemas)
end
@doc """
Cast all params in `Plug.Conn` to conform to the schemas for `OpenApiSpex.Operation`.
Returns `{:ok, Plug.Conn.t}` with `params` and `body_params` fields updated if successful,
or `{:error, reason}` if casting fails.
`content_type` may optionally be supplied to select the `requestBody` schema.
"""
@spec cast(OpenApi.t, Operation.t, Plug.Conn.t, content_type | nil) :: {:ok, Plug.Conn.t} | {:error, String.t}
when content_type: String.t
def cast(spec = %OpenApi{}, operation = %Operation{}, conn = %Plug.Conn{}, content_type \\ nil) do
Operation.cast(operation, conn, content_type, spec.components.schemas)
end
@doc """
Validate params against `OpenApiSpex.Schema`.
See `OpenApiSpex.Schema.validate/3` for examples of error messages.
"""
@spec validate(OpenApi.t, Schema.t | Reference.t, any) :: :ok | {:error, String.t}
def validate(spec = %OpenApi{}, schema = %Schema{}, params) do
Schema.validate(schema, params, spec.components.schemas)
end
def validate(spec = %OpenApi{}, schema = %Reference{}, params) do
Schema.validate(schema, params, spec.components.schemas)
end
@doc """
Validate all params in `Plug.Conn` against `OpenApiSpex.Operation` `parameter` and `requestBody` schemas.
`content_type` may be optionally supplied to select the `requestBody` schema.
"""
@spec validate(OpenApi.t, Operation.t, Plug.Conn.t, content_type | nil) :: :ok | {:error, String.t}
when content_type: String.t
def validate(spec = %OpenApi{}, operation = %Operation{}, conn = %Plug.Conn{}, content_type \\ nil) do
Operation.validate(operation, conn, content_type, spec.components.schemas)
end
+ def path_to_string(%CastError{} = error) do
+ CastError.path_to_string(error)
+ end
+
@doc """
Declares a struct based `OpenApiSpex.Schema`
- defines the schema/0 callback
- ensures the schema is linked to the module by "x-struct" extension property
- defines a struct with keys matching the schema properties
- defines a @type `t` for the struct
- derives a `Poison.Encoder` for the struct
See `OpenApiSpex.Schema` for additional examples and details.
## Example
require OpenApiSpex
defmodule User do
OpenApiSpex.schema %{
title: "User",
description: "A user of the app",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "User ID"},
name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/},
email: %Schema{type: :string, description: "Email address", format: :email},
inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :'date-time'},
updated_at: %Schema{type: :string, description: "Update timestamp", format: :'date-time'}
},
required: [:name, :email],
example: %{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
}
end
"""
defmacro schema(body) do
quote do
@behaviour OpenApiSpex.Schema
@schema struct(OpenApiSpex.Schema, Map.put(unquote(body), :"x-struct", __MODULE__))
def schema, do: @schema
@derive [Poison.Encoder]
defstruct Schema.properties(@schema)
@type t :: %__MODULE__{}
end
end
end
diff --git a/lib/open_api_spex/cast.ex b/lib/open_api_spex/cast.ex
index 10b4f47..0b513ef 100644
--- a/lib/open_api_spex/cast.ex
+++ b/lib/open_api_spex/cast.ex
@@ -1,41 +1,38 @@
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}
-
- with {:error, [error | _]} <- cast(ctx) do
- {:error, to_string(error)}
- end
+ cast(ctx)
end
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
def cast(%CastContext{value: nil, schema: %{nullable: true}}) do
{:ok, nil}
end
def cast(%CastContext{value: nil} = ctx) do
CastContext.error(ctx, {:null_value})
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 30e5800..10d7aaf 100644
--- a/lib/open_api_spex/cast_error.ex
+++ b/lib/open_api_spex/cast_error.ex
@@ -1,95 +1,107 @@
defmodule OpenApiSpex.CastError do
alias OpenApiSpex.TermType
defstruct reason: nil,
value: nil,
format: nil,
type: nil,
name: nil,
path: []
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, {: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, {: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} = ctx) do
+ def message(%{reason: :null_value} = error) do
message =
- case ctx.type do
+ case error.type do
nil -> "null value"
type -> "null value where #{type} expected"
end
- prepend_path(message, ctx)
+ message
end
- def message(%{reason: :invalid_type, type: type, value: value} = ctx) do
- prepend_path("Invalid #{type}. Got: #{TermType.type(value)}", ctx)
+ def message(%{reason: :invalid_type, type: type, value: value}) do
+ "Invalid #{type}. Got: #{TermType.type(value)}"
end
- def message(%{reason: :polymorphic_failed, type: polymorphic_type} = ctx) do
- prepend_path("Failed to cast to any schema in #{polymorphic_type}", ctx)
+ def message(%{reason: :invalid_format, format: format}) do
+ "Invalid format. Expected #{inspect(format)}"
end
- def message(%{reason: :unexpected_field, name: name} = ctx) do
- prepend_path("Unexpected field: #{safe_string(name)}", ctx)
+ def message(%{reason: :polymorphic_failed, type: polymorphic_type}) do
+ "Failed to cast to any schema in #{polymorphic_type}"
end
- def message(%{reason: :no_value_required_for_discriminator, name: field} = ctx) do
- prepend_path("No value for required disciminator property: #{field}", ctx)
+ def message(%{reason: :unexpected_field, name: name}) do
+ "Unexpected field: #{safe_string(name)}"
end
- def message(%{reason: :unknown_schema, name: name} = ctx) do
- prepend_path("Unknown schema: #{name}", ctx)
+ def message(%{reason: :no_value_required_for_discriminator, name: field}) do
+ "No value for required disciminator property: #{field}"
end
- def message(%{reason: :missing_field, name: name} = ctx) do
- prepend_path("Missing field: #{name}", ctx)
+ 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(message, ctx) do
+ defp prepend_path(error, message) do
path =
- case ctx.path do
+ case error.path do
[] -> "#"
- _ -> "#/" <> (ctx.path |> Enum.map(&to_string/1) |> Path.join())
+ _ -> "#" <> path_to_string(error)
end
path <> ": " <> message
end
defp safe_string(string) do
- to_string(string) |> String.slice(1..40)
+ 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_object.ex b/lib/open_api_spex/cast_object.ex
index 1f16155..28e17fa 100644
--- a/lib/open_api_spex/cast_object.ex
+++ b/lib/open_api_spex/cast_object.ex
@@ -1,88 +1,88 @@
defmodule OpenApiSpex.CastObject do
@moduledoc false
alias OpenApiSpex.{Cast, CastError, CastContext}
def cast(%{value: value} = ctx) when not is_map(value) do
CastContext.error(ctx, {:invalid_type, :object})
end
def cast(%{value: value, schema: %{properties: nil}}) do
{:ok, value}
end
def cast(%{value: value, schema: schema} = ctx) do
schema_properties = schema.properties || %{}
with :ok <- check_unrecognized_properties(ctx, schema_properties),
value <- cast_atom_keys(value, schema_properties),
ctx = %{ctx | value: value},
:ok <- check_required_fields(ctx, schema),
:ok <- cast_properties(%{ctx | schema: schema_properties}) do
{:ok, value}
end
end
defp check_unrecognized_properties(%{value: value} = ctx, expected_keys) do
input_keys = value |> Map.keys() |> Enum.map(&to_string/1)
schema_keys = expected_keys |> Map.keys() |> Enum.map(&to_string/1)
extra_keys = input_keys -- schema_keys
if extra_keys == [] do
:ok
else
[name | _] = extra_keys
CastContext.error(ctx, {:unexpected_field, name})
end
end
defp check_required_fields(%{value: input_map} = ctx, schema) do
required = schema.required || []
input_keys = Map.keys(input_map)
missing_keys = required -- input_keys
if missing_keys == [] do
:ok
else
errors =
Enum.map(missing_keys, fn key ->
ctx = %{ctx | path: [key | ctx.path]}
CastError.new(ctx, {:missing_field, key})
end)
- {:error, errors ++ ctx.errors}
+ {:error, ctx.errors ++ errors}
end
end
defp cast_atom_keys(input_map, properties) do
Enum.reduce(properties, %{}, fn {key, _}, output ->
string_key = to_string(key)
case input_map do
%{^key => value} -> Map.put(output, key, value)
%{^string_key => value} -> Map.put(output, key, value)
_ -> output
end
end)
end
defp cast_properties(%{value: object, schema: schema_properties} = ctx) do
Enum.reduce(object, {:ok, %{}}, fn
{key, value}, {:ok, output} ->
cast_property(%{ctx | key: key, value: value, schema: schema_properties}, output)
_, error ->
error
end)
end
defp cast_property(%{key: key, schema: schema_properties} = ctx, output) do
prop_schema = Map.get(schema_properties, key)
path = [key | ctx.path]
with {:error, errors} <- Cast.cast(%{ctx | path: path, schema: prop_schema}) do
{:error, errors}
else
{:ok, value} -> {:ok, Map.put(output, key, value)}
end
end
end
diff --git a/test/cast_test.exs b/test/cast_test.exs
index f2fe9fa..4b6f90b 100644
--- a/test/cast_test.exs
+++ b/test/cast_test.exs
@@ -1,183 +1,183 @@
defmodule OpenApiSpec.CastTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, CastContext, CastError, Reference, Schema}
def cast(ctx), do: Cast.cast(struct(CastContext, ctx))
describe "cast/3" do
# Note: full tests for primitives are covered in CastPrimitiveTest
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 to_string(error) == "#: null value where array expected"
+ assert CastError.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 %CastError{} = 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 [%CastError{} = error] = errors
assert error.reason == :invalid_type
assert error.value == "two"
assert error.path == [1]
end
# Additional object tests found in CastObjectTest
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 %CastError{} = 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 %CastError{} = error
assert error.path == [:data, :age]
- assert to_string(error) == "#/data/age: Invalid integer. Got: string"
+ assert CastError.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 %CastError{} = error
assert error.path == [:data, 1, :age]
- assert to_string(error) == "#/data/1/age: Invalid integer. Got: string"
+ assert CastError.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 %CastError{} = error
assert error.reason == :invalid_type
assert error.path == [1]
- assert to_string(error) == "#/1: Invalid integer. Got: string"
+ assert CastError.message_with_path(error) == "#/1: Invalid integer. Got: string"
- assert to_string(error2) == "#/3: Invalid integer. Got: string"
+ assert CastError.message_with_path(error2) == "#/3: Invalid integer. Got: string"
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Nov 30, 2:48 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41474
Default Alt Text
(19 KB)

Event Timeline