Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F113053
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
23 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Nov 24, 7:20 AM (23 h, 42 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39367
Default Alt Text
(23 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment