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