Page MenuHomePhorge

No OneTemporary

Size
23 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index 4470776..e399d84 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,292 +1,302 @@
defmodule OpenApiSpex.Schema do
alias OpenApiSpex.{
Schema, Reference, Discriminator, Xml, ExternalDocumentation
}
+
defstruct [
:title,
:multipleOf,
:maximum,
:exclusiveMaximum,
:minimum,
:exclusiveMinimum,
:maxLength,
:minLength,
:pattern,
:maxItems,
:minItems,
:uniqueItems,
:maxProperties,
:minProperties,
:required,
:enum,
:type,
:allOf,
:oneOf,
:anyOf,
:not,
:items,
:properties,
:additionalProperties,
:description,
:format,
:default,
:nullable,
:discriminator,
:readOnly,
:writeOnly,
:xml,
:externalDocs,
:example,
- :deprecated
+ :deprecated,
+ :"x-struct"
]
@type t :: %__MODULE__{
title: String.t,
multipleOf: number,
maximum: number,
exclusiveMaximum: boolean,
minimum: number,
exclusiveMinimum: boolean,
maxLength: integer,
minLength: integer,
pattern: String.t,
maxItems: integer,
minItems: integer,
uniqueItems: boolean,
maxProperties: integer,
minProperties: integer,
required: [String.t],
enum: [String.t],
type: String.t,
allOf: [Schema.t | Reference.t],
oneOf: [Schema.t | Reference.t],
anyOf: [Schema.t | Reference.t],
not: Schema.t | Reference.t,
items: Schema.t | Reference.t,
properties: %{String.t => Schema.t | Reference.t},
additionalProperties: boolean | Schema.t | Reference.t,
description: String.t,
format: String.t,
default: any,
nullable: boolean,
discriminator: Discriminator.t,
readOnly: boolean,
writeOnly: boolean,
xml: Xml.t,
externalDocs: ExternalDocumentation.t,
example: any,
- deprecated: boolean
+ deprecated: boolean,
+ "x-struct": module
}
- defp resolve_schema(schema = %Schema{}, _), do: schema
- defp resolve_schema(%Reference{"$ref": "#/components/schemas/" <> name}, schemas), do: schemas[name]
+ def resolve_schema(schema = %Schema{}, _), do: schema
+ def resolve_schema(%Reference{"$ref": "#/components/schemas/" <> name}, schemas), do: schemas[name]
+
+ def cast(schema = %Schema{"x-struct": mod}, value, schemas) when not is_nil(mod) do
+ with {:ok, data} <- cast(%{schema | "x-struct": nil}, value, schemas) do
+ {:ok, struct(mod, data)}
+ end
+ end
def cast(%Schema{type: :boolean}, value, _schemas) when is_boolean(value), do: {:ok, value}
def cast(%Schema{type: :boolean}, value, _schemas) when is_binary(value) do
case value do
"true" -> true
"false" -> false
_ -> {:error, "Invalid boolean: #{inspect(value)}"}
end
end
def cast(%Schema{type: :integer}, value, _schemas) when is_integer(value), do: {:ok, value}
def cast(%Schema{type: :integer}, value, _schemas) when is_binary(value) do
case Integer.parse(value) do
{i, ""} -> {:ok, i}
_ -> {:error, :bad_integer}
end
end
def cast(%Schema{type: :number}, value, _schemas) when is_number(value), do: {:ok, value}
def cast(%Schema{type: :number}, value, _schemas) when is_binary(value) do
case Float.parse(value) do
{x, ""} -> {:ok, x}
_ -> {:error, :bad_float}
end
end
def cast(%Schema{type: :string, format: :"date-time"}, value, _schemas) when is_binary(value) do
case DateTime.from_iso8601(value) do
{:ok, datetime = %DateTime{}, _offset} -> {:ok, datetime}
error = {:error, _reason} -> error
end
end
def cast(%Schema{type: :string, format: :date}, value, _schemas) when is_binary(value) do
case Date.from_iso8601(value) do
{:ok, date = %Date{}} -> {:ok, date}
error = {:error, _reason} -> error
end
end
def cast(%Schema{type: :string}, value, _schemas) when is_binary(value), do: {:ok, value}
def cast(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value), do: {:ok, value}
def cast(%Schema{type: :array}, [], _schemas), do: {:ok, []}
def cast(schema = %Schema{type: :array, items: items_schema}, [x | rest], schemas) do
- case cast(items_schema, x, schemas) do
- {:ok, x_cast} -> [x_cast | cast(schema, rest, schemas)]
- error -> error
+ with {:ok, x_cast} <- cast(items_schema, x, schemas),
+ {:ok, rest_cast} <- cast(schema, rest, schemas) do
+ {:ok, [x_cast | rest_cast]}
end
end
def cast(%Schema{type: :object, properties: properties}, value, schemas) when is_map(value) do
properties
|> Stream.filter(fn {name, _} -> Map.has_key?(value, name) || Map.has_key?(value, Atom.to_string(name)) end)
|> Stream.map(fn {name, schema} -> {name, resolve_schema(schema, schemas)} end)
|> Stream.map(fn {name, schema} -> {name, schema, Map.get(value, name, value[Atom.to_string(name)])} end)
|> Stream.map(fn {name, schema, property_val} -> cast_property(name, schema, property_val, schemas) end)
|> Enum.reduce({:ok, %{}}, fn
_, {:error, reason} -> {:error, reason}
{:error, reason}, _ -> {:error, reason}
{:ok, {name, property_val}}, {:ok, acc} -> {:ok, Map.put(acc, name, property_val)}
end)
end
def cast(ref = %Reference{}, val, schemas), do: cast(resolve_schema(ref, schemas), val, schemas)
defp cast_property(name, schema, value, schemas) do
casted = cast(schema, value, schemas)
case casted do
{:ok, new_value} -> {:ok, {name, new_value}}
{:error, reason} -> {:error, reason}
end
end
def validate(ref = %Reference{}, val, schemas), do: validate(resolve_schema(ref, schemas), val, schemas)
def validate(schema = %Schema{type: type}, value, _schemas) when type in [:integer, :number] do
with :ok <- validate_multiple(schema, value),
:ok <- validate_maximum(schema, value),
:ok <- validate_minimum(schema, value) do
:ok
end
end
def validate(schema = %Schema{type: :string}, value, _schemas) do
with :ok <- validate_max_length(schema, value),
:ok <- validate_min_length(schema, value),
:ok <- validate_pattern(schema, value) do
:ok
end
end
def validate(%Schema{type: :boolean}, value, _schemas) do
case is_boolean(value) do
true -> :ok
_ -> {:error, "Invalid boolean: #{inspect(value)}"}
end
end
def validate(schema = %Schema{type: :array}, value, schemas) do
with :ok <- validate_max_items(schema, value),
:ok <- validate_min_items(schema, value),
:ok <- validate_unique_items(schema, value),
:ok <- validate_array_items(schema, value, schemas) do
:ok
end
end
def validate(schema = %Schema{type: :object, properties: properties}, value, schemas) do
with :ok <- validate_required_properties(schema, value),
:ok <- validate_max_properties(schema, value),
:ok <- validate_min_properties(schema, value),
:ok <- validate_object_properties(properties, value, schemas) do
:ok
end
end
def validate_multiple(%{multipleOf: nil}, _), do: :ok
def validate_multiple(%{multipleOf: n}, value) when (round(value / n) * n == value), do: :ok
def validate_multiple(%{multipleOf: n}, value), do: {:error, "#{value} is not a multiple of #{n}"}
def validate_maximum(%{maximum: nil}, _), do: :ok
def validate_maximum(%{maximum: n, exclusiveMaximum: true}, value) when value < n, do: :ok
def validate_maximum(%{maximum: n}, value) when value <= n, do: :ok
def validate_maximum(%{maximum: n}, value), do: {:error, "#{value} is larger than maximum #{n}"}
def validate_minimum(%{minimum: nil}, _), do: :ok
def validate_minimum(%{minimum: n, exclusiveMinimum: true}, value) when value > n, do: :ok
def validate_minimum(%{minimum: n}, value) when value >= n, do: :ok
def validate_minimum(%{minimum: n}, value), do: {:error, "#{value} is smaller than minimum #{n}"}
def validate_max_length(%{maxLength: nil}, _), do: :ok
def validate_max_length(%{maxLength: n}, value) do
case String.length(value) <= n do
true -> :ok
_ -> {:error, "String length is larger than maxLength: #{n}"}
end
end
def validate_min_length(%{minLength: nil}, _), do: :ok
def validate_min_length(%{minLength: n}, value) do
case String.length(value) >= n do
true -> :ok
_ -> {:error, "String length is smaller than minLength: #{n}"}
end
end
def validate_pattern(%{pattern: nil}, _), do: :ok
def validate_pattern(schema = %{pattern: regex}, val) when is_binary(regex) do
validate_pattern(%{schema | pattern: Regex.compile(regex)}, val)
end
def validate_pattern(%{pattern: regex = %Regex{}}, val) do
case Regex.match?(regex, val) do
true -> :ok
_ -> {:error, "Value does not match pattern: #{regex.source}"}
end
end
def validate_max_items(%Schema{maxItems: nil}, _), do: :ok
def validate_max_items(%Schema{maxItems: n}, value) when length(value) <= n, do: :ok
def validate_max_items(%Schema{maxItems: n}, value) do
{:error, "Array length #{length(value)} is larger than maxItems: #{n}"}
end
def validate_min_items(%Schema{minItems: nil}, _), do: :ok
def validate_min_items(%Schema{minItems: n}, value) when length(value) >= n, do: :ok
def validate_min_items(%Schema{minItems: n}, value) do
{:error, "Array length #{length(value)} is smaller than minItems: #{n}"}
end
def validate_unique_items(%Schema{uniqueItems: true}, value) do
unique_size =
value
|> MapSet.new()
|> MapSet.size()
case unique_size == length(value) do
true -> :ok
_ -> {:error, "Array items must be unique"}
end
end
+ def validate_unique_items(_, _), do: :ok
- def validate_array_items(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value), do: {:ok, value}
- def validate_array_items(%Schema{type: :array}, [], _schemas), do: {:ok, []}
+ def validate_array_items(%Schema{type: :array, items: nil}, value, _schemas) when is_list(value), do: :ok
+ def validate_array_items(%Schema{type: :array}, [], _schemas), do: :ok
def validate_array_items(schema = %Schema{type: :array, items: item_schema}, [x | rest], schemas) do
with :ok <- validate(item_schema, x, schemas) do
validate(schema, rest, schemas)
end
end
def validate_required_properties(%Schema{type: :object, required: nil}, _), do: :ok
def validate_required_properties(%Schema{type: :object, required: required}, value) do
missing = required -- Map.keys(value)
case missing do
[] -> :ok
_ -> {:error, "Missing required properties: #{inspect(missing)}"}
end
end
def validate_max_properties(%Schema{type: :object, maxProperties: nil}, _), do: :ok
def validate_max_properties(%Schema{type: :object, maxProperties: n}, val) when map_size(val) <= n, do: :ok
def validate_max_properties(%Schema{type: :object, maxProperties: n}, val) do
{:error, "Object property count #{map_size(val)} is greater than maxProperties: #{n}"}
end
def validate_min_properties(%Schema{type: :object, minProperties: nil}, _), do: :ok
def validate_min_properties(%Schema{type: :object, minProperties: n}, val) when map_size(val) >= n, do: :ok
def validate_min_properties(%Schema{type: :object, minProperties: n}, val) do
{:error, "Object property count #{map_size(val)} is less than minProperties: #{n}"}
end
def validate_object_properties(properties = %{}, value, schemas) do
properties
|> Enum.filter(fn {name, _schema} -> Map.has_key?(value, name) end)
|> validate_object_properties(value, schemas)
end
def validate_object_properties([], _, _), do: :ok
def validate_object_properties([{name, schema} | rest], value, schemas) do
- case validate(schema, value[name], schemas) do
+ case validate(schema, Map.fetch!(value, name), schemas) do
:ok -> validate_object_properties(rest, value, schemas)
error -> error
end
end
end
\ No newline at end of file
diff --git a/test/open_api_spex_test.exs b/test/open_api_spex_test.exs
index 7e24f47..f023368 100644
--- a/test/open_api_spex_test.exs
+++ b/test/open_api_spex_test.exs
@@ -1,57 +1,67 @@
defmodule OpenApiSpexTest do
use ExUnit.Case
alias OpenApiSpexTest.ApiSpec
describe "OpenApi" do
test "compete" do
spec = ApiSpec.spec()
assert spec
end
test "Valid Request" do
request_body = %{
"user" => %{
"id" => 123,
"name" => "asdf",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
conn =
:post
|> Plug.Test.conn("/api/users", Poison.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json")
+ |> OpenApiSpexTest.Router.call([])
- conn = OpenApiSpexTest.Router.call(conn, [])
- assert conn.params == %{
- user: %{
+ assert conn.params == %OpenApiSpexTest.Schemas.UserRequest{
+ user: %OpenApiSpexTest.Schemas.User{
id: 123,
name: "asdf",
email: "foo@bar.com",
updated_at: ~N[2017-09-12T14:44:55Z] |> DateTime.from_naive!("Etc/UTC")
}
}
+
+ assert Poison.decode!(conn.resp_body) == %{
+ "data" => %{
+ "email" => "foo@bar.com",
+ "id" => 1234,
+ "inserted_at" => nil,
+ "name" => "asdf",
+ "updated_at" => "2017-09-12T14:44:55Z"
+ }
+ }
end
test "Invalid Request" do
request_body = %{
"user" => %{
"id" => 123,
"name" => "*1234",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
conn =
:post
|> Plug.Test.conn("/api/users", Poison.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json")
conn = OpenApiSpexTest.Router.call(conn, [])
assert conn.status == 422
assert conn.resp_body == "Value does not match pattern: [a-zA-Z][a-zA-Z0-9_]+"
end
end
end
\ No newline at end of file
diff --git a/test/schema_test.exs b/test/schema_test.exs
index abf688e..f9aa3d7 100644
--- a/test/schema_test.exs
+++ b/test/schema_test.exs
@@ -1,31 +1,46 @@
defmodule OpenApiSpex.SchemaTest do
use ExUnit.Case
alias OpenApiSpex.Schema
alias OpenApiSpexTest.ApiSpec
test "cast request schema" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
user_request_schema = schemas["UserRequest"]
input = %{
"user" => %{
"id" => 123,
"name" => "asdf",
"email" => "foo@bar.com",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
{:ok, output} = Schema.cast(user_request_schema, input, schemas)
- assert output == %{
- user: %{
+ assert output == %OpenApiSpexTest.Schemas.UserRequest{
+ user: %OpenApiSpexTest.Schemas.User{
id: 123,
name: "asdf",
email: "foo@bar.com",
updated_at: DateTime.from_naive!(~N[2017-09-12T14:44:55], "Etc/UTC")
}
}
end
+
+ def assert_example_matches_schema(schema_module) do
+ alias OpenApiSpex.{Schema, SchemaResolver}
+ {reference, schemas} = SchemaResolver.resolve_schema_modules_from_schema(schema_module, %{})
+ schema = Schema.resolve_schema(reference, schemas)
+ assert {:ok, data} = Schema.cast(schema, schema.example, schemas)
+ assert :ok = Schema.validate(schema, data, schemas)
+ end
+
+ test "User Schema example matches schema" do
+ assert_example_matches_schema(OpenApiSpexTest.Schemas.User)
+ assert_example_matches_schema(OpenApiSpexTest.Schemas.UserRequest)
+ assert_example_matches_schema(OpenApiSpexTest.Schemas.UserResponse)
+ assert_example_matches_schema(OpenApiSpexTest.Schemas.UsersResponse)
+ end
end
\ No newline at end of file
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index 9a839cf..84b4a52 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,59 +1,124 @@
defmodule OpenApiSpexTest.Schemas do
alias OpenApiSpex.Schema
defmodule User do
- def schema do
- %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'}
- }
- }
- end
+ @derive [Poison.Encoder]
+ @schema %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"
+ },
+ "x-struct": __MODULE__
+ }
+ def schema, do: @schema
+ defstruct Map.keys(@schema.properties)
+
+ @type t :: %__MODULE__{
+ id: integer,
+ name: String.t,
+ email: String.t,
+ inserted_at: DateTime.t,
+ updated_at: DateTime.t
+ }
end
defmodule UserRequest do
- def schema do
- %Schema{
- title: "UserRequest",
- description: "POST body for creating a user",
- type: :object,
- properties: %{
- user: User
+ @derive [Poison.Encoder]
+ @schema %Schema{
+ title: "UserRequest",
+ description: "POST body for creating a user",
+ type: :object,
+ properties: %{
+ user: User
+ },
+ example: %{
+ "user" => %{
+ "name" => "Joe User",
+ "email" => "joe@gmail.com"
}
- }
- end
+ },
+ "x-struct": __MODULE__
+ }
+ def schema, do: @schema
+ defstruct Map.keys(@schema.properties)
+
+ @type t :: %__MODULE__{
+ user: User.t
+ }
end
defmodule UserResponse do
- def schema do
- %Schema{
- title: "UserResponse",
- description: "Response schema for single user",
- type: :object,
- properties: %{
- data: User
+ @derive [Poison.Encoder]
+ @schema %Schema{
+ title: "UserResponse",
+ description: "Response schema for single user",
+ type: :object,
+ properties: %{
+ data: User
+ },
+ example: %{
+ "data" => %{
+ "id" => 123,
+ "name" => "Joe User",
+ "email" => "joe@gmail.com",
+ "inserted_at" => "2017-09-12T12:34:55Z",
+ "updated_at" => "2017-09-13T10:11:12Z"
}
- }
- end
+ },
+ "x-struct": __MODULE__
+ }
+ def schema, do: @schema
+ defstruct Map.keys(@schema.properties)
+
+ @type t :: %__MODULE__{
+ data: User.t
+ }
end
defmodule UsersResponse do
- def schema do
- %Schema{
- title: "UsersReponse",
- description: "Response schema for multiple users",
- type: :object,
- properties: %{
- data: %Schema{description: "The users details", type: :array, items: User}
- }
- }
- end
+ @derive [Poison.Encoder]
+ @schema %Schema{
+ title: "UsersReponse",
+ description: "Response schema for multiple users",
+ type: :object,
+ properties: %{
+ data: %Schema{description: "The users details", type: :array, items: User}
+ },
+ example: %{
+ "data" => [
+ %{
+ "id" => 123,
+ "name" => "Joe User",
+ "email" => "joe@gmail.com"
+ },
+ %{
+ "id" => 456,
+ "name" => "Jay Consumer",
+ "email" => "jay@yahoo.com"
+ }
+ ]
+ },
+ "x-struct": __MODULE__
+ }
+ def schema, do: @schema
+ defstruct Map.keys(@schema.properties)
+
+ @type t :: %__MODULE__{
+ data: [User.t]
+ }
end
end
\ No newline at end of file
diff --git a/test/support/user_controller.ex b/test/support/user_controller.ex
index b0f3e63..8d22c2f 100644
--- a/test/support/user_controller.ex
+++ b/test/support/user_controller.ex
@@ -1,70 +1,85 @@
defmodule OpenApiSpexTest.UserController do
use Phoenix.Controller
alias OpenApiSpex.Operation
alias OpenApiSpexTest.Schemas
- alias Plug.Conn
plug OpenApiSpex.Plug.Cast
plug OpenApiSpex.Plug.Validate
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
+ @doc """
+ API Spec for :show action
+ """
def show_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "Show user",
description: "Show a user by ID",
operationId: "UserController.show",
parameters: [
parameter(:id, :path, :integer, "User ID", example: 123, minimum: 1)
],
responses: %{
200 => response("User", "application/json", Schemas.UserResponse)
}
}
end
- def show(conn, _params) do
- conn
- |> Conn.send_resp(200, "HELLO")
+ def show(conn, %{id: id}) do
+ json(conn, %Schemas.UserResponse{
+ data: %Schemas.User{
+ id: id,
+ name: "joe user",
+ email: "joe@gmail.com"
+ }
+ })
end
def index_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "List users",
description: "List all useres",
operationId: "UserController.index",
parameters: [],
responses: %{
200 => response("User List Response", "application/json", Schemas.UsersResponse)
}
}
end
def index(conn, _params) do
- conn
- |> Conn.send_resp(200, "HELLO")
+ json(conn, %Schemas.UsersResponse{
+ data: [
+ %Schemas.User{
+ id: 123,
+ name: "joe user",
+ email: "joe@gmail.com"
+ }
+ ]
+ })
end
def create_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "Create user",
description: "Create a user",
operationId: "UserController.create",
parameters: [],
requestBody: request_body("The user attributes", "application/json", Schemas.UserRequest),
responses: %{
201 => response("User", "application/json", Schemas.UserResponse)
}
}
end
- def create(conn, _params) do
- conn
- |> Conn.send_resp(201, "DONE")
+ def create(conn, %Schemas.UserRequest{user: user = %Schemas.User{}}) do
+ json(conn, %Schemas.UserResponse{
+ data: %{user | id: 1234}
+ })
end
end
\ No newline at end of file

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 24, 7:20 AM (21 h, 44 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39367
Default Alt Text
(23 KB)

Event Timeline