Page MenuHomePhorge

No OneTemporary

Size
29 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index efaed8e..7890ad2 100644
--- a/README.md
+++ b/README.md
@@ -1,210 +1,249 @@
# Open API Spex
Add Open API Specification 3 (formerly swagger) to Plug applications.
## Installation
The package can be installed by adding `open_api_spex` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:open_api_spex, github: "mbuhot/open_api_spex"}
]
end
```
## Generating an API spec
Start by adding an `ApiSpec` module to your application.
```elixir
defmodule MyApp.ApiSpec do
alias OpenApiSpex.{OpenApi, Server, Info, Paths}
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(MyAppWeb.Endpoint, otp_app: :my_app)
],
info: %Info{
title: "My App",
version: "1.0"
},
# populate the paths from a phoenix router
paths: Paths.from_router(MyAppWeb.Router)
}
|> OpenApiSpex.resolve_schema_modules() # discover request/response schemas from path specs
end
end
```
For each plug (controller) that will handle api requests, add an `open_api_operation` callback.
It will be passed the plug opts that were declared in the router, this will be the action for a phoenix controller.
```elixir
defmodule MyApp.UserController do
alias OpenApiSpex.Operation
+ alias MyApp.Schemas.UserResponse
@spec open_api_operation(any) :: Operation.t
def open_api_operation(action), do: apply(__MODULE__, :"#{action}_operation", [])
@spec show_operation() :: Operation.t
def show_operation() do
%Operation{
tags: ["users"],
summary: "Show user",
description: "Show a user by ID",
operationId: "UserController.show",
parameters: [
Operation.parameter(:id, :path, :integer, "User ID", example: 123)
],
responses: %{
- 200 => Operation.response("User", "application/json", Schemas.UserResponse)
+ 200 => Operation.response("User", "application/json", UserResponse)
}
}
end
def show(conn, %{"id" => id}) do
{:ok, user} = MyApp.Users.find_by_id(id)
json(conn, 200, user)
end
end
```
Declare the JSON schemas for request/response bodies in a `Schemas` module:
+Each module should export a `schema/0` function, and may optionally declare a struct,
+linked to the JSON schema through the `x-struct` extension property.
```elixir
defmodule MyApp.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"},
- email: %Schema{type: :string, description: "Email address", format: :email},
- inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :datetime},
- updated_at: %Schema{type: :string, description: "Update timestamp", format: :datetime}
- }
+ @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"},
+ email: %Schema{type: :string, description: "Email address", format: :email},
+ inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :datetime},
+ updated_at: %Schema{type: :string, description: "Update timestamp", format: :datetime}
+ },
+ required: [:name, :email],
+ example: {
+ "id" => 123,
+ "name" => "Joe",
+ "email" => "joe@gmail.com"
}
- end
+ "x-struct": __MODULE__
+ }
+ def schema, do: @schema
+ defstruct Map.keys(@schema.properties)
end
defmodule UserResponse do
- def schema do
- %Schema{
- title: "UserResponse",
- description: "Response schema for single user",
- type: :object,
- properties: %{
- data: User
- }
- }
- end
+ @schema %Schema{
+ title: "UserResponse",
+ description: "Response schema for single user",
+ type: :object,
+ properties: %{
+ data: User
+ },
+ "x-struct": __MODULE__
+ }
+ def schema, do: @schema
+ defstruct Map.keys(@schema.properties)
end
end
```
Now you can create a mix task to write the swagger file to disk:
```elixir
defmodule Mix.Tasks.MyApp.OpenApiSpec do
def run([output_file]) do
json =
MyApp.ApiSpec.spec()
|> Poison.encode!(pretty: true)
:ok = File.write!(output_file, json)
end
end
```
Generate the file with: `mix myapp.openapispec spec.json`
## Serving the API Spec from a Controller
To serve the API spec from your application, first add the `OpenApiSpex.Plug.PutApiSpec` plug somewhere in the pipeline.
```elixir
pipeline :api do
plug OpenApiSpex.Plug.PutApiSpec, module: MyApp.ApiSpec
end
```
Now the spec will be available for use in downstream plugs.
The `OpenApiSpex.Plug.RenderSpec` plug will render the spec as JSON:
```elixir
scope "/api" do
pipe_through :api
resources "/users", MyApp.UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
```
## Use the API Spec to cast params
Add the `OpenApiSpex.Plug.Cast` plug to a controller to cast the request parameters to elixir types defined by the operation schema.
```elixir
plug OpenApiSpex.Plug.Cast, operation_id: "UserController.show"
```
The `operation_id` can be inferred when used from a Phoenix controller from the contents of `conn.private`.
```elixir
defmodule MyApp.UserController do
use MyAppWeb, :controller
alias OpenApiSpex.Operation
+ alias MyApp.Schemas.{User, UserRequest, UserResponse}
plug OpenApiSpex.Plug.Cast
def open_api_operation(action) do
apply(__MODULE__, "#{action}_operation", [])
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),
+ requestBody: request_body("The user attributes", "application/json", UserRequest),
responses: %{
- 201 => response("User", "application/json", Schemas.UserResponse)
+ 201 => response("User", "application/json", UserResponse)
}
}
end
- def create(conn, %{user: %{name: name, email: email, birthday: birthday = %Date{}}}) do
- # params will have atom keys with values cast to standard elixir types
+ def create(conn, %UserRequest{user: %User{name: name, email: email, birthday: birthday = %Date{}}}) do
+ # params are cast to UserRequest struct
end
end
```
## Use the API Spec to validate Requests
Add both the `Cast` and `Validate` plugs to your controller / plug:
```elixir
plug OpenApiSpex.Plug.Cast
plug OpenApiSpex.Plug.Validate
```
Now the client will receive a 422 response whenever the request fails to meet the validation rules from the api spec.
+## Validating Examples from Schemas
-TODO: SwaggerUI 3.0
+As schemas evolve, you may want to confirm that the examples given match the schemas.
+
+```elixir
+Use ExUnit.Case
+import OpenApiSpex.Test.Assertions
+
+test "UsersResponse example matches schema" do
+ api_spec = MyApp.ApiSpec.spec()
+ schema = api_spec.component.schemas["UsersResponse"]
+ assert_schema(schema.example, "UsersResponse", api_spec)
+end
+```
+
+## Validating API responses in Tests
+
+```elixir
+use MyApp.ConnCase
-TODO: Validating examples in the spec
+test "UserController produces a UsersResponse", %{conn: conn} do
+ api_spec = MyApp.ApiSpec.spec()
+ json =
+ conn
+ |> get(user_path(conn, :index))
+ |> json_response(200)
-TODO: Validating responses in tests
+ assert_schema(json, "UsersResponse", api_spec)
+end
+```
+
+TODO: SwaggerUI 3.0
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index c73794f..5182451 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,36 +1,36 @@
defmodule OpenApiSpex do
alias OpenApiSpex.{OpenApi, Operation, Reference, Schema, SchemaResolver}
@moduledoc """
"""
def resolve_schema_modules(spec = %OpenApi{}) do
SchemaResolver.resolve_schema_modules(spec)
end
@doc """
Cast params to conform to a Schema or Operation spec.
"""
def cast(spec = %OpenApi{}, schema = %Schema{}, params) do
- Schema.cast(schema, params, spec.compnents.schemas)
+ Schema.cast(schema, params, spec.components.schemas)
end
def cast(spec = %OpenApi{}, schema = %Reference{}, params) do
- Schema.cast(schema, params, spec.compnents.schemas)
+ Schema.cast(schema, params, spec.components.schemas)
end
def cast(spec = %OpenApi{}, operation = %Operation{}, params, content_type \\ nil) do
Operation.cast(operation, params, content_type, spec.components.schemas)
end
@doc """
Validate params against a Schema or Operation spec.
"""
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
def validate(spec = %OpenApi{}, operation = %Operation{}, params = %{}, content_type \\ nil) do
Operation.validate(operation, params, content_type, spec.components.schemas)
end
end
diff --git a/lib/open_api_spex/schema.ex b/lib/open_api_spex/schema.ex
index 0fea192..b22fd38 100644
--- a/lib/open_api_spex/schema.ex
+++ b/lib/open_api_spex/schema.ex
@@ -1,299 +1,300 @@
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, true},
:description,
:format,
:default,
:nullable,
:discriminator,
:readOnly,
:writeOnly,
:xml,
:externalDocs,
:example,
: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,
"x-struct": module
}
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
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 = %Schema{type: :object}, value, schemas) when is_map(value) do
with {:ok, props} <- cast_properties(schema, Enum.to_list(value), schemas) do
{:ok, Map.new(props)}
end
end
def cast(ref = %Reference{}, val, schemas), do: cast(resolve_schema(ref, schemas), val, schemas)
def cast(additionalProperties, val, _schemas) when is_boolean(additionalProperties), do: val
defp cast_properties(%Schema{}, [], _schemas), do: {:ok, []}
defp cast_properties(object_schema = %Schema{}, [{key, value} | rest], schemas) do
{name, schema} = Enum.find(
object_schema.properties,
{key, object_schema.additionalProperties},
fn {name, _schema} -> to_string(name) == to_string(key) end)
with {:ok, new_value} <- cast(schema, value, schemas),
{:ok, cast_tail} <- cast_properties(object_schema, rest, schemas) do
{:ok, [{name, new_value} | cast_tail]}
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
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, 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/lib/open_api_spex/test/assertions.ex b/lib/open_api_spex/test/assertions.ex
new file mode 100644
index 0000000..eaf9fec
--- /dev/null
+++ b/lib/open_api_spex/test/assertions.ex
@@ -0,0 +1,15 @@
+defmodule OpenApiSpex.Test.Assertions do
+ alias OpenApiSpex.OpenApi
+ import ExUnit.Assertions
+
+ def assert_schema(value = %{}, schema_title, api_spec = %OpenApi{}) do
+ schemas = api_spec.components.schemas
+ schema = schemas[schema_title]
+ if !schema do
+ flunk("Schema: #{schema_title} not found in #{inspect(Map.keys(schemas))}")
+ end
+
+ assert {:ok, data} = OpenApiSpex.cast(api_spec, schema, value)
+ assert :ok = OpenApiSpex.validate(api_spec, schema, data)
+ end
+end
\ No newline at end of file
diff --git a/test/schema_resolver_test.exs b/test/schema_resolver_test.exs
index f99e896..9d23375 100644
--- a/test/schema_resolver_test.exs
+++ b/test/schema_resolver_test.exs
@@ -1,70 +1,70 @@
defmodule OpenApiSpex.SchemaResolverTest do
use ExUnit.Case
alias OpenApiSpex.{
MediaType,
OpenApi,
Operation,
PathItem,
Reference,
RequestBody,
Response,
Schema
}
test "Resolves schemas in OpenApi spec" do
spec = %OpenApi{
paths: %{
"/api/users" => %PathItem{
get: %Operation{
responses: %{
200 => %Response{
content: %{
"application/json" => %MediaType{
schema: OpenApiSpexTest.Schemas.UsersResponse
}
}
}
}
},
post: %Operation{
description: "Create a user",
operationId: "UserController.create",
requestBody: %RequestBody{
content: %{
"application/json" => %MediaType{
schema: OpenApiSpexTest.Schemas.UserRequest
}
}
},
responses: %{
201 => %Response{
content: %{
"application/json" => %MediaType{
schema: OpenApiSpexTest.Schemas.UserResponse
}
}
}
}
}
}
}
}
resolved = OpenApiSpex.resolve_schema_modules(spec)
- assert %Reference{"$ref": "#/components/schemas/UsersReponse"} =
+ assert %Reference{"$ref": "#/components/schemas/UsersResponse"} =
resolved.paths["/api/users"].get.responses[200].content["application/json"].schema
assert %Reference{"$ref": "#/components/schemas/UserResponse"} =
resolved.paths["/api/users"].post.responses[201].content["application/json"].schema
assert %Reference{"$ref": "#/components/schemas/UserRequest"} =
resolved.paths["/api/users"].post.requestBody.content["application/json"].schema
assert %{
"UserRequest" => %Schema{},
"UserResponse" => %Schema{},
"User" => %Schema{},
} = resolved.components.schemas
end
end
\ No newline at end of file
diff --git a/test/schema_test.exs b/test/schema_test.exs
index f9aa3d7..0fa5ee9 100644
--- a/test/schema_test.exs
+++ b/test/schema_test.exs
@@ -1,46 +1,40 @@
defmodule OpenApiSpex.SchemaTest do
use ExUnit.Case
alias OpenApiSpex.Schema
- alias OpenApiSpexTest.ApiSpec
+ alias OpenApiSpexTest.{ApiSpec, Schemas}
+ import OpenApiSpex.Test.Assertions
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 == %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)
+ spec = ApiSpec.spec()
+ assert_schema(Schemas.User.schema().example, "User", spec)
+ assert_schema(Schemas.UserRequest.schema().example, "UserRequest", spec)
+ assert_schema(Schemas.UserResponse.schema().example, "UserResponse", spec)
+ assert_schema(Schemas.UsersResponse.schema().example, "UsersResponse", spec)
end
end
\ No newline at end of file
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index 84b4a52..3e12255 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,124 +1,124 @@
defmodule OpenApiSpexTest.Schemas do
alias OpenApiSpex.Schema
defmodule User do
@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
@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"
}
},
"x-struct": __MODULE__
}
def schema, do: @schema
defstruct Map.keys(@schema.properties)
@type t :: %__MODULE__{
user: User.t
}
end
defmodule UserResponse do
@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"
}
},
"x-struct": __MODULE__
}
def schema, do: @schema
defstruct Map.keys(@schema.properties)
@type t :: %__MODULE__{
data: User.t
}
end
defmodule UsersResponse do
@derive [Poison.Encoder]
@schema %Schema{
- title: "UsersReponse",
+ title: "UsersResponse",
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

File Metadata

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

Event Timeline