Page MenuHomePhorge

No OneTemporary

Size
83 KB
Referenced Files
None
Subscribers
None
diff --git a/README.md b/README.md
index ad4a141..c3419c5 100644
--- a/README.md
+++ b/README.md
@@ -1,384 +1,384 @@
# Open API Spex
[![Build Status](https://travis-ci.com/open-api-spex/open_api_spex.svg?branch=master)](https://travis-ci.com/open-api-spex/open_api_spex)
[![Hex.pm](https://img.shields.io/hexpm/v/open_api_spex.svg)](https://hex.pm/packages/open_api_spex)
Leverage Open Api Specification 3 (swagger) to document, test, validate and explore your Plug and Phoenix APIs.
- Generate and serve a JSON Open Api Spec document from your code
- Use the spec to cast request params to well defined schema structs
- Validate params against schemas, eliminate bad requests before they hit your controllers
- Validate responses against schemas in tests, ensuring your docs are accurate and reliable
- Explore the API interactively with with [SwaggerUI](https://swagger.io/swagger-ui/)
Full documentation available on [hexdocs](https://hexdocs.pm/open_api_spex/)
## 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, "~> 3.4"}
]
end
```
## Generate Spec
Start by adding an `ApiSpec` module to your application to populate an `OpenApiSpex.OpenApi` struct.
```elixir
defmodule MyAppWeb.ApiSpec do
alias OpenApiSpex.{OpenApi, Server, Info, Paths}
alias MyAppWeb.{Endpoint, Router}
@behaviour OpenApi
@impl OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
Server.from_endpoint(Endpoint)
],
info: %Info{
title: "My App",
version: "1.0"
},
# populate the paths from a phoenix router
paths: Paths.from_router(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. The callback populates an `OpenApiSpex.Operation` struct describing the plug/action.
```elixir
defmodule MyAppWeb.UserController do
alias OpenApiSpex.Operation
alias MyAppWeb.Schemas.UserResponse
@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
@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, required: true)
],
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
}
end
# Controller's `show` action
def show(conn, %{id: id}) do
{:ok, user} = MyApp.Users.find_by_id(id)
json(conn, 200, user)
end
end
```
Alternatively, you can create an operation file separately using `defdelegate`.
```elixir
# Phoenix's controller
defmodule MyAppWeb.UserController do
defdelegate open_api_operation(action), to: MyAppWeb.UserApiOperation
def show(conn, %{id: id}) do
{:ok, user} = MyApp.Users.find_by_id(id)
json(conn, 200, user)
end
end
# Open API Spex operations
defmodule MyAppWeb.UserApiOperation do
alias OpenApiSpex.Operation
alias MyAppWeb.Schemas.UserResponse
@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
@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, required: true)
],
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
}
end
end
```
For examples of other action operations, see the
[example web app](https://github.com/open-api-spex/open_api_spex/blob/master/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex).
Next, declare JSON schema modules for the request and response bodies.
In each schema module, call `OpenApiSpex.schema/1`, passing the schema definition. The schema must
have keys described in `OpenApiSpex.Schema.t`. This will define a `%OpenApiSpex.Schema{}` struct.
This struct is made available from the `schema/0` public function, which is generated by `OpenApiSpex.schema/1`.
You may optionally have the data described by the schema turned into a struct linked to the JSON schema by adding `"x-struct": __MODULE__`
to the schema.
```elixir
defmodule MyAppWeb.Schemas do
alias OpenApiSpex.Schema
defmodule User do
require OpenApiSpex
OpenApiSpex.schema(%{
title: "User",
description: "A user of the app",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "User ID"},
name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/},
email: %Schema{type: :string, description: "Email address", format: :email},
birthday: %Schema{type: :string, description: "Birth date", format: :date},
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",
"birthday" => "1970-01-01T12:34:55Z",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
})
end
defmodule UserResponse do
require OpenApiSpex
OpenApiSpex.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",
"birthday" => "1970-01-01T12:34:55Z",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
}
})
end
end
```
For more examples of schema definitions, see the
[sample Phoenix app](https://github.com/open-api-spex/open_api_spex/blob/master/examples/phoenix_app/lib/phoenix_app_web/schemas.ex)
## Serve the Spec
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: MyAppWeb.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", MyAppWeb.UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
```
## Generating the Spec
Optionally, 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
MyAppWeb.Endpoint.start_link() # Required if using for OpenApiSpex.Server.from_endpoint/1
json =
MyAppWeb.ApiSpec.spec()
|> Jason.encode!(pretty: true)
:ok = File.write!(output_file, json)
end
end
```
Generate the file with: `mix my_app.openapispec spec.json`
## Serve Swagger UI
Once your API spec is available through a route (see "Serve the Spec"), the `OpenApiSpex.Plug.SwaggerUI` plug can be used to
serve a SwaggerUI interface. The `path:` plug option must be supplied to give the path to the API spec.
All JavaScript and CSS assets are sourced from cdnjs.cloudflare.com, rather than vendoring into this package.
```elixir
scope "/" do
pipe_through :browser # Use the default browser stack
get "/", MyAppWeb.PageController, :index
get "/swaggerui", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi"
end
scope "/api" do
pipe_through :api
resources "/users", MyAppWeb.UserController, only: [:create, :index, :show]
get "/openapi", OpenApiSpex.Plug.RenderSpec, []
end
```
## Validating and Casting Params
OpenApiSpex can automatically validate requests before they reach the controller action function. Or if you prefer,
you can explicitly call on OpenApiSpex to cast and validate the params within the controller action. This section
describes the former.
First, the `plug OpenApiSpex.Plug.PutApiSpec` needs to be called in the Router, as described above.
Add the `OpenApiSpex.Plug.CastAndValidate` plug to a controller to validate request parameters and to cast to Elixir types defined by the operation schema.
```elixir
# Phoenix
plug OpenApiSpex.Plug.CastAndValidate
# Plug
plug OpenApiSpex.Plug.CastAndValidate, operation_id: "UserController.create
```
For Phoenix apps, the `operation_id` can be inferred from the contents of `conn.private`.
```elixir
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
alias OpenApiSpex.Operation
alias MyAppWeb.Schemas.{User, UserRequest, UserResponse}
plug OpenApiSpex.Plug.CastAndValidate
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: [
parameter(:id, :query, :integer, "user ID")
],
requestBody: request_body("The user attributes", "application/json", UserRequest),
responses: %{
201 => response("User", "application/json", UserResponse)
}
}
end
def create(conn = %{body_params: %UserRequest{user: %User{name: name, email: email, birthday: birthday = %Date{}}}}, %{id: id}) do
# conn.body_params cast to UserRequest struct
# conn.params.id cast to integer
end
end
```
Now the client will receive a 422 response whenever the request fails to meet the validation rules from the api spec.
The response body will include the validation error message:
```json
{
"errors": [
{
"message": "Invalid format. Expected :date",
"source": {
"pointer": "/data/birthday"
},
"title": "Invalid value"
}
]
}
```
-See also `OpenApiSpex.cast_and_validate/3` and `OpenApiSpex.Cast.cast/3` for more examples outside of a `plug` pipeline.
+See also `OpenApiSpex.cast_value/3` for casting and validating outside of a `plug` pipeline.
## Validate Examples
As schemas evolve, you may want to confirm that the examples given match the schemas.
-Use the `OpenApiSpex.Test.Assertions` module to assert on schema validations.
+Use the `OpenApiSpex.TestAssertions` module to assert on schema validations.
```elixir
use ExUnit.Case
-import OpenApiSpex.Test.Assertions
+import OpenApiSpex.TestAssertions
test "UsersResponse example matches schema" do
api_spec = MyAppWeb.ApiSpec.spec()
schema = MyAppWeb.Schemas.UsersResponse.schema()
assert_schema(schema.example, "UsersResponse", api_spec)
end
```
## Validate Responses
-API responses can be tested against schemas using `OpenApiSpex.Test.Assertions` also:
+API responses can be tested against schemas using `OpenApiSpex.TestAssertions` also:
```elixir
use MyAppWeb.ConnCase
-import OpenApiSpex.Test.Assertions
+import OpenApiSpex.TestAssertions
test "UserController produces a UsersResponse", %{conn: conn} do
api_spec = MyAppWeb.ApiSpec.spec()
json =
conn
|> get(user_path(conn, :index))
|> json_response(200)
assert_schema(json, "UsersResponse", api_spec)
end
```
diff --git a/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex b/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex
index ee5a49f..0446588 100644
--- a/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex
+++ b/examples/phoenix_app/lib/phoenix_app_web/controllers/user_controller.ex
@@ -1,84 +1,83 @@
defmodule PhoenixAppWeb.UserController do
use PhoenixAppWeb, :controller
import OpenApiSpex.Operation, only: [parameter: 5, request_body: 4, response: 3]
alias OpenApiSpex.Operation
alias PhoenixApp.{Accounts, Accounts.User}
alias PhoenixAppWeb.Schemas
- plug(OpenApiSpex.Plug.Cast)
- plug(OpenApiSpex.Plug.Validate)
+ plug OpenApiSpex.Plug.CastAndValidate
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
def index_operation() do
%Operation{
tags: ["users"],
summary: "List users",
description: "List all useres",
operationId: "UserController.index",
responses: %{
200 => response("User List Response", "application/json", Schemas.UsersResponse)
}
}
end
def index(conn, _params) do
users = Accounts.list_users()
render(conn, "index.json", users: users)
end
def create_operation() do
%Operation{
tags: ["users"],
summary: "Create user",
description: "Create a user",
operationId: "UserController.create",
parameters: [
Operation.parameter(:group_id, :path, :integer, "Group ID", example: 1)
],
requestBody:
request_body("The user attributes", "application/json", Schemas.UserRequest,
required: true
),
responses: %{
201 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def create(conn = %{body_params: %Schemas.UserRequest{user: user_params}}, %{
group_id: _group_id
}) do
with {:ok, %User{} = user} <- Accounts.create_user(user_params) do
conn
|> put_status(:created)
|> put_resp_header("location", user_path(conn, :show, user))
|> render("show.json", user: user)
end
end
@doc """
API Spec for :show action
"""
def show_operation() do
%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, required: true)
],
responses: %{
200 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def show(conn, %{id: id}) do
user = Accounts.get_user!(id)
render(conn, "show.json", user: user)
end
end
diff --git a/examples/phoenix_app/mix.exs b/examples/phoenix_app/mix.exs
index 4abf84c..8466b90 100644
--- a/examples/phoenix_app/mix.exs
+++ b/examples/phoenix_app/mix.exs
@@ -1,49 +1,49 @@
defmodule PhoenixApp.Mixfile do
use Mix.Project
def project do
[
app: :phoenix_app,
version: "0.0.1",
elixir: "~> 1.5",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix] ++ Mix.compilers(),
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
end
# Configuration for the OTP application.
#
# Type `mix help compile.app` for more information.
def application do
[
mod: {PhoenixApp.Application, []},
extra_applications: [:logger, :runtime_tools]
]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp aliases() do
[test: ["ecto.create --quiet", "ecto.migrate", "test"]]
end
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:open_api_spex, path: "../../"},
{:ecto, "~> 2.2"},
{:sqlite_ecto2, "~> 2.4"},
{:phoenix, "~> 1.4"},
{:plug_cowboy, "~> 2.0"},
{:jason, "~> 1.0"},
- {:dialyxir, "1.0.0-rc.6"}
+ {:dialyxir, "1.0.0-rc.6", only: [:dev], runtime: false}
]
end
end
diff --git a/examples/phoenix_app/priv/repo/.gitignore b/examples/phoenix_app/priv/repo/.gitignore
new file mode 100644
index 0000000..1550c57
--- /dev/null
+++ b/examples/phoenix_app/priv/repo/.gitignore
@@ -0,0 +1 @@
+phoenix_app_test.*
diff --git a/examples/phoenix_app/test/phoenix_app_web/controllers/user_controller_test.exs b/examples/phoenix_app/test/phoenix_app_web/controllers/user_controller_test.exs
index ee3b8ad..d5a085e 100644
--- a/examples/phoenix_app/test/phoenix_app_web/controllers/user_controller_test.exs
+++ b/examples/phoenix_app/test/phoenix_app_web/controllers/user_controller_test.exs
@@ -1,57 +1,65 @@
defmodule PhoenixAppWeb.UserControllerTest do
use PhoenixAppWeb.ConnCase, async: true
- import OpenApiSpex.Test.Assertions
+ import OpenApiSpex.TestAssertions
setup do
%{spec: PhoenixAppWeb.ApiSpec.spec()}
end
test "create user", %{conn: conn, spec: spec} do
conn
|> Plug.Conn.put_req_header("content-type", "application/json")
- |> post(user_path(conn, :create, 1), %{"user" => %{"name" => "Joe", "email" => "joe@gmail.com"}})
+ |> post(user_path(conn, :create, 1), %{
+ "user" => %{"name" => "Joe", "email" => "joe@gmail.com"}
+ })
|> json_response(201)
|> assert_schema("UserResponse", spec)
end
test "get user", %{conn: conn, spec: spec} do
- %{id: id} = PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Carl", email: "Carl@yahoo.com"})
+ %{id: id} =
+ PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Carl", email: "Carl@yahoo.com"})
conn
|> Plug.Conn.put_req_header("accept", "application/json")
|> get(user_path(conn, :show, id))
|> json_response(200)
|> assert_schema("UserResponse", spec)
end
test "list users", %{conn: conn, spec: spec} do
- %{id: id1} = PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Aaron", email: "aaron@hotmail.com"})
- %{id: id2} = PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Benjamin", email: "ben@lycos.com"})
- %{id: id3} = PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Chuck", email: "chuck@aol.com"})
+ %{id: id1} =
+ PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Aaron", email: "aaron@hotmail.com"})
+
+ %{id: id2} =
+ PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Benjamin", email: "ben@lycos.com"})
+
+ %{id: id3} =
+ PhoenixApp.Repo.insert!(%PhoenixApp.Accounts.User{name: "Chuck", email: "chuck@aol.com"})
response =
conn
|> Plug.Conn.put_req_header("accept", "application/json")
|> get(user_path(conn, :index))
|> json_response(200)
|> assert_schema("UsersResponse", spec)
assert [%{id: ^id1}, %{id: ^id2}, %{id: ^id3}] = response.data
end
test "User schema example", %{spec: spec} do
assert_schema(PhoenixAppWeb.Schemas.User.schema().example, "User", spec)
end
test "UserRequest schema example", %{spec: spec} do
assert_schema(PhoenixAppWeb.Schemas.UserRequest.schema().example, "UserRequest", spec)
end
test "UserResponse schema example", %{spec: spec} do
assert_schema(PhoenixAppWeb.Schemas.UserResponse.schema().example, "UserResponse", spec)
end
test "UsersResponse schema example", %{spec: spec} do
assert_schema(PhoenixAppWeb.Schemas.UsersResponse.schema().example, "UsersResponse", spec)
end
end
diff --git a/examples/plug_app/priv/repo/.gitignore b/examples/plug_app/priv/repo/.gitignore
new file mode 100644
index 0000000..570fb92
--- /dev/null
+++ b/examples/plug_app/priv/repo/.gitignore
@@ -0,0 +1 @@
+plug_app_test.*
diff --git a/lib/open_api_spex.ex b/lib/open_api_spex.ex
index 1a42fef..1801f4e 100644
--- a/lib/open_api_spex.ex
+++ b/lib/open_api_spex.ex
@@ -1,232 +1,255 @@
defmodule OpenApiSpex do
@moduledoc """
Provides the entry-points for defining schemas, validating and casting.
"""
alias OpenApiSpex.{
Components,
OpenApi,
Operation,
Operation2,
Reference,
Schema,
SchemaException,
SchemaResolver,
SchemaConsistency
}
alias OpenApiSpex.Cast.Error
@doc """
Adds schemas to the api spec from the modules specified in the Operations.
Eg, if the response schema for an operation is defined with:
responses: %{
200 => Operation.response("User", "application/json", UserResponse)
}
Then the `UserResponse.schema()` function will be called to load the schema, and
a `Reference` to the loaded schema will be used in the operation response.
See `OpenApiSpex.schema` macro for a convenient syntax for defining schema modules.
"""
@spec resolve_schema_modules(OpenApi.t()) :: OpenApi.t()
def resolve_schema_modules(spec = %OpenApi{}) do
SchemaResolver.resolve_schema_modules(spec)
end
+ @doc """
+ Cast and validate a value against a given Schema.
+ """
+ def cast_value(value, schema = %schema_mod{}) when schema_mod in [Schema, Reference] do
+ OpenApiSpex.Cast.cast(schema, value)
+ end
+
+ @doc """
+ Cast and validate a value against a given Schema belonging to a given OpenApi spec.
+ """
+ def cast_value(value, schema = %schema_mod{}, spec = %OpenApi{})
+ when schema_mod in [Schema, Reference] do
+ OpenApiSpex.Cast.cast(schema, value, spec.components.schemas)
+ end
+
def cast_and_validate(
spec = %OpenApi{},
operation = %Operation{},
conn = %Plug.Conn{},
content_type \\ nil
) do
Operation2.cast(operation, conn, content_type, spec.components)
end
@doc """
Cast params to conform to a `OpenApiSpex.Schema`.
See `OpenApiSpex.Schema.cast/3` for additional examples and details.
"""
@spec cast(OpenApi.t(), Schema.t() | Reference.t(), any) :: {:ok, any} | {:error, String.t()}
+ @deprecated "Use OpenApiSpex.cast_value/3 or cast_value/2 instead"
def cast(spec = %OpenApi{}, schema = %Schema{}, params) do
Schema.cast(schema, params, spec.components.schemas)
end
def cast(spec = %OpenApi{}, schema = %Reference{}, params) do
Schema.cast(schema, params, spec.components.schemas)
end
@doc """
Cast all params in `Plug.Conn` to conform to the schemas for `OpenApiSpex.Operation`.
Returns `{:ok, Plug.Conn.t}` with `params` and `body_params` fields updated if successful,
or `{:error, reason}` if casting fails.
`content_type` may optionally be supplied to select the `requestBody` schema.
"""
@spec cast(OpenApi.t(), Operation.t(), Plug.Conn.t(), content_type | nil) ::
{:ok, Plug.Conn.t()} | {:error, String.t()}
when content_type: String.t()
+ @deprecated "Use OpenApiSpex.cast_and_validate/3 instead"
def cast(spec = %OpenApi{}, operation = %Operation{}, conn = %Plug.Conn{}, content_type \\ nil) do
Operation.cast(operation, conn, content_type, spec.components.schemas)
end
@doc """
Validate params against `OpenApiSpex.Schema`.
See `OpenApiSpex.Schema.validate/3` for examples of error messages.
"""
@spec validate(OpenApi.t(), Schema.t() | Reference.t(), any) :: :ok | {:error, String.t()}
+ @deprecated "Use OpenApiSpex.cast_value/3 or cast_value/2 instead"
def validate(spec = %OpenApi{}, schema = %Schema{}, params) do
Schema.validate(schema, params, spec.components.schemas)
end
def validate(spec = %OpenApi{}, schema = %Reference{}, params) do
Schema.validate(schema, params, spec.components.schemas)
end
@doc """
Validate all params in `Plug.Conn` against `OpenApiSpex.Operation` `parameter` and `requestBody` schemas.
`content_type` may be optionally supplied to select the `requestBody` schema.
"""
@spec validate(OpenApi.t(), Operation.t(), Plug.Conn.t(), content_type | nil) ::
:ok | {:error, String.t()}
when content_type: String.t()
+ @deprecated "Use OpenApiSpex.cast_and_validate/4 instead"
def validate(
spec = %OpenApi{},
operation = %Operation{},
conn = %Plug.Conn{},
content_type \\ nil
) do
Operation.validate(operation, conn, content_type, spec.components.schemas)
end
def path_to_string(%Error{} = error) do
Error.path_to_string(error)
end
+ def error_message(%Error{} = error) do
+ Error.message(error)
+ end
+
@doc """
Declares a struct based `OpenApiSpex.Schema`
- defines the schema/0 callback
- ensures the schema is linked to the module by "x-struct" extension property
- defines a struct with keys matching the schema properties
- defines a @type `t` for the struct
- derives a `Jason.Encoder` and/or `Poison.Encoder` for the struct
See `OpenApiSpex.Schema` for additional examples and details.
## Example
require OpenApiSpex
defmodule User do
OpenApiSpex.schema %{
title: "User",
description: "A user of the app",
type: :object,
properties: %{
id: %Schema{type: :integer, description: "User ID"},
name: %Schema{type: :string, description: "User name", pattern: ~r/[a-zA-Z][a-zA-Z0-9_]+/},
email: %Schema{type: :string, description: "Email address", format: :email},
inserted_at: %Schema{type: :string, description: "Creation timestamp", format: :'date-time'},
updated_at: %Schema{type: :string, description: "Update timestamp", format: :'date-time'}
},
required: [:name, :email],
example: %{
"id" => 123,
"name" => "Joe User",
"email" => "joe@gmail.com",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
}
end
"""
defmacro schema(body) do
quote do
@compile {:report_warnings, false}
@behaviour OpenApiSpex.Schema
@schema struct(
OpenApiSpex.Schema,
Map.put(Map.delete(unquote(body), :__struct__), :"x-struct", __MODULE__)
)
def schema, do: @schema
@derive Enum.filter([Poison.Encoder, Jason.Encoder], &Code.ensure_loaded?/1)
defstruct Schema.properties(@schema)
@type t :: %__MODULE__{}
Map.from_struct(@schema) |> OpenApiSpex.validate_compiled_schema()
# Throwing warnings to prevent runtime bugs (like in issue #144)
@schema
|> SchemaConsistency.warnings()
|> Enum.each(&IO.warn("Inconsistent schema: #{&1}", Macro.Env.stacktrace(__ENV__)))
end
end
@doc """
Validate the compiled schema's properties to ensure the schema is not improperly
defined. Only errors which would cause a given schema to _always_ fail should be
raised here.
"""
def validate_compiled_schema(schema) do
Enum.each(schema, fn prop_and_val ->
:ok = validate_compiled_schema(prop_and_val, schema)
end)
end
def validate_compiled_schema({_, %Schema{} = schema}, _parent) do
validate_compiled_schema(schema)
end
@doc """
Used for validating the schema at compile time, otherwise we're forced
to raise errors for improperly defined schemas at runtime.
"""
def validate_compiled_schema({:discriminator, %{propertyName: property, mapping: _}}, %{
anyOf: schemas
})
when is_list(schemas) do
Enum.each(schemas, fn schema ->
case schema do
%Schema{title: title} when is_binary(title) -> :ok
_ -> error!(:discriminator_schema_missing_title, schema, property_name: property)
end
end)
end
def validate_compiled_schema({:discriminator, %{propertyName: _, mapping: _}}, schema) do
case {schema.anyOf, schema.allOf, schema.oneOf} do
{nil, nil, nil} ->
error!(:discriminator_missing_composite_key, schema)
_ ->
:ok
end
end
def validate_compiled_schema({_property, _value}, _schema), do: :ok
@doc """
Raises compile time errors for improperly defined schemas.
"""
@spec error!(atom(), Schema.t(), keyword()) :: no_return()
@spec error!(atom(), Schema.t()) :: no_return()
def error!(error, schema, details \\ []) do
raise SchemaException, %{error: error, schema: schema, details: details}
end
@doc """
Resolve a schema or reference to a schema.
"""
@spec resolve_schema(Schema.t() | Reference.t(), Components.schemas_map()) :: Schema.t()
def resolve_schema(%Schema{} = schema, _), do: schema
def resolve_schema(%Reference{} = ref, schemas), do: Reference.resolve_schema(ref, schemas)
end
diff --git a/lib/open_api_spex/plug/cast.ex b/lib/open_api_spex/plug/cast.ex
index 70b2a38..75a5f43 100644
--- a/lib/open_api_spex/plug/cast.ex
+++ b/lib/open_api_spex/plug/cast.ex
@@ -1,92 +1,110 @@
defmodule OpenApiSpex.Plug.Cast do
@moduledoc """
Module plug that will cast the `Conn.params` and `Conn.body_params` according to the schemas defined for the operation.
Note that when using this plug, the body params are no longer merged into `Conn.params` and must be read from `Conn.body_params`
separately.
The operation_id can be given at compile time as an argument to `init`:
plug OpenApiSpex.Plug.Cast, operation_id: "MyApp.ShowUser"
For phoenix applications, the operation_id can be obtained at runtime automatically.
defmodule MyAppWeb.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.Cast
...
end
If you want customize the error response, you can provide the `:render_error` option to register a plug which creates
a custom response in the case of a validation error.
## Example
defmodule MyAppWeb.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.Cast,
render_error: MyApp.RenderError
...
end
defmodule MyApp.RenderError do
def init(opts), do: opts
def call(conn, reason) do
msg = %{error: reason} |> Posion.encode!()
conn
|> Conn.put_resp_content_type("application/json")
|> Conn.send_resp(400, msg)
end
end
"""
@behaviour Plug
alias Plug.Conn
@impl Plug
+ @deprecated "Use OpenApiSpex.Plug.CastAndValidate instead"
def init(opts) do
opts
|> Map.new()
|> Map.put_new(:render_error, OpenApiSpex.Plug.DefaultRenderError)
end
@impl Plug
- def call(conn = %{private: %{open_api_spex: private_data}}, %{operation_id: operation_id, render_error: render_error}) do
+ @deprecated "Use OpenApiSpex.Plug.CastAndValidate instead"
+ def call(conn = %{private: %{open_api_spex: private_data}}, %{
+ operation_id: operation_id,
+ render_error: render_error
+ }) do
spec = private_data.spec
operation = private_data.operation_lookup[operation_id]
- content_type = Conn.get_req_header(conn, "content-type")
- |> Enum.at(0, "")
- |> String.split(";")
- |> Enum.at(0)
+
+ content_type =
+ Conn.get_req_header(conn, "content-type")
+ |> Enum.at(0, "")
+ |> String.split(";")
+ |> Enum.at(0)
+
private_data = Map.put(private_data, :operation_id, operation_id)
conn = Conn.put_private(conn, :open_api_spex, private_data)
- case OpenApiSpex.cast(spec, operation, conn, content_type) do
- {:ok, conn} -> conn
+ case apply(OpenApiSpex, :cast, [spec, operation, conn, content_type]) do
+ {:ok, conn} ->
+ conn
+
{:error, reason} ->
opts = render_error.init(reason)
conn
|> render_error.call(opts)
|> Plug.Conn.halt()
end
end
- def call(conn = %{private: %{phoenix_controller: controller, phoenix_action: action, open_api_spex: _pd}}, opts) do
+
+ def call(
+ conn = %{
+ private: %{phoenix_controller: controller, phoenix_action: action, open_api_spex: _pd}
+ },
+ opts
+ ) do
operation_id = controller.open_api_operation(action).operationId
- if (operation_id) do
+
+ if operation_id do
call(conn, Map.put(opts, :operation_id, operation_id))
else
raise "operationId was not found in action API spec"
end
end
+
def call(_conn = %{private: %{open_api_spex: _pd}}, _opts) do
raise ":operation_id was neither provided nor inferred from conn. Consider putting plug OpenApiSpex.Plug.Cast rather into your phoenix controller."
end
+
def call(_conn, _opts) do
raise ":open_api_spex was not found under :private. Maybe OpenApiSpex.Plug.PutApiSpec was not called before?"
end
-
end
diff --git a/lib/open_api_spex/plug/validate.ex b/lib/open_api_spex/plug/validate.ex
index 54e9fd5..8e6d1e4 100644
--- a/lib/open_api_spex/plug/validate.ex
+++ b/lib/open_api_spex/plug/validate.ex
@@ -1,84 +1,88 @@
defmodule OpenApiSpex.Plug.Validate do
@moduledoc """
Module plug that validates params against the schema defined for an operation.
If validation fails, the plug will send a 422 response with the reason as the body.
This plug should always be run after `OpenApiSpex.Plug.Cast`, as it expects the params map to
have atom keys and query params converted from strings to the appropriate types.
## Example
defmodule MyApp.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.Cast
plug OpenApiSpex.Plug.Validate
...
end
If you want customize the error response, you can provide the `:render_error` option to register a plug which creates
a custom response in the case of a validation error.
## Example
defmodule MyApp.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.Cast
plug OpenApiSpex.Plug.Validate,
render_error: MyApp.RenderError
def render_error(conn, reason) do
msg = %{error: reason} |> Posion.encode!()
conn
|> Conn.put_resp_content_type("application/json")
|> Conn.send_resp(400, msg)
end
...
end
defmodule MyApp.RenderError do
def init(opts), do: opts
def call(conn, reason) do
msg = %{error: reason} |> Posion.encode!()
conn
|> Conn.put_resp_content_type("application/json")
|> Conn.send_resp(400, msg)
end
end
"""
@behaviour Plug
alias Plug.Conn
@impl Plug
+ @deprecated "Use OpenApiSpex.Plug.CastAndValidate.init/1 instead"
def init(opts), do: Keyword.put_new(opts, :render_error, OpenApiSpex.Plug.DefaultRenderError)
@impl Plug
+ @deprecated "Use OpenApiSpex.Plug.CastAndValidate.call/2 instead"
def call(conn, render_error: render_error) do
spec = conn.private.open_api_spex.spec
operation_id = conn.private.open_api_spex.operation_id
operation_lookup = conn.private.open_api_spex.operation_lookup
operation = operation_lookup[operation_id]
- content_type = Conn.get_req_header(conn, "content-type")
- |> Enum.at(0, "")
- |> String.split(";")
- |> Enum.at(0)
- with :ok <- OpenApiSpex.validate(spec, operation, conn, content_type) do
+ content_type =
+ Conn.get_req_header(conn, "content-type")
+ |> Enum.at(0, "")
+ |> String.split(";", parts: 2)
+ |> Enum.at(0)
+
+ with :ok <- apply(OpenApiSpex, :validate, [spec, operation, conn, content_type]) do
conn
else
{:error, reason} ->
opts = render_error.init(reason)
conn
|> render_error.call(opts)
|> Plug.Conn.halt()
end
end
def render_error(conn, reason) do
conn |> Conn.send_resp(422, "#{reason}")
end
-end
\ No newline at end of file
+end
diff --git a/lib/open_api_spex/test/assertions.ex b/lib/open_api_spex/test/assertions.ex
index 411019d..552bde7 100644
--- a/lib/open_api_spex/test/assertions.ex
+++ b/lib/open_api_spex/test/assertions.ex
@@ -1,37 +1,42 @@
defmodule OpenApiSpex.Test.Assertions do
@moduledoc """
Defines helpers for testing API responses and examples against API spec schemas.
"""
alias OpenApiSpex.OpenApi
import ExUnit.Assertions
@dialyzer {:no_match, assert_schema: 3}
@doc """
Asserts that `value` conforms to the schema with title `schema_title` in `api_spec`.
"""
- @spec assert_schema(map, String.t, OpenApi.t) :: map | no_return
+ @spec assert_schema(map, String.t(), OpenApi.t()) :: map | no_return
+ @deprecated "Use OpenApiSpex.TestAssertions.assert_schema/3 instead"
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
-
data =
- case OpenApiSpex.cast(api_spec, schema, value) do
- {:ok, data} -> data
+ case Kernel.apply(OpenApiSpex, :cast, [api_spec, schema, value]) do
+ {:ok, data} ->
+ data
+
{:error, reason} ->
- flunk("Value does not conform to schema #{schema_title}: #{reason}\n#{inspect value}")
+ flunk("Value does not conform to schema #{schema_title}: #{reason}\n#{inspect(value)}")
end
- case OpenApiSpex.validate(api_spec, schema, data) do
- :ok -> :ok
+ case Kernel.apply(OpenApiSpex, :validate, [api_spec, schema, value]) do
+ :ok ->
+ :ok
+
{:error, reason} ->
- flunk("Value does not conform to schema #{schema_title}: #{reason}\n#{inspect value}")
+ flunk("Value does not conform to schema #{schema_title}: #{reason}\n#{inspect(value)}")
end
data
end
end
diff --git a/lib/open_api_spex/test/assertions2.ex b/lib/open_api_spex/test/assertions2.ex
index 38e2b37..9ef82f9 100644
--- a/lib/open_api_spex/test/assertions2.ex
+++ b/lib/open_api_spex/test/assertions2.ex
@@ -1,42 +1,12 @@
defmodule OpenApiSpex.Test.Assertions2 do
@moduledoc """
Defines helpers for testing API responses and examples against API spec schemas.
"""
- import ExUnit.Assertions
- alias OpenApiSpex.{Cast, OpenApi}
- alias OpenApiSpex.Cast.Error
-
- @dialyzer {:no_match, assert_schema: 3}
@doc """
Asserts that `value` conforms to the schema with title `schema_title` in `api_spec`.
"""
@spec assert_schema(map, String.t(), OpenApi.t()) :: map | no_return
- 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
-
- case Cast.cast(schema, value, api_spec.components.schemas) do
- {:ok, data} ->
- data
-
- {:error, errors} ->
- errors =
- Enum.map(errors, fn error ->
- message = Error.message(error)
- path = Error.path_to_string(error)
- "#{message} at #{path}"
- end)
-
- flunk(
- "Value does not conform to schema #{schema_title}: #{Enum.join(errors, "\n")}\n#{
- inspect(value)
- }"
- )
- end
- end
+ @deprecated "Use OpenApiSpex.TestAssertions.assert_schema/3 instead"
+ defdelegate assert_schema(value, schema_title, api_spec), to: OpenApiSpex.TestAssertions
end
diff --git a/lib/open_api_spex/test/assertions2.ex b/lib/open_api_spex/test/test_assertions.ex
similarity index 88%
copy from lib/open_api_spex/test/assertions2.ex
copy to lib/open_api_spex/test/test_assertions.ex
index 38e2b37..cb728cb 100644
--- a/lib/open_api_spex/test/assertions2.ex
+++ b/lib/open_api_spex/test/test_assertions.ex
@@ -1,42 +1,42 @@
-defmodule OpenApiSpex.Test.Assertions2 do
+defmodule OpenApiSpex.TestAssertions do
@moduledoc """
Defines helpers for testing API responses and examples against API spec schemas.
"""
import ExUnit.Assertions
- alias OpenApiSpex.{Cast, OpenApi}
+ alias OpenApiSpex.OpenApi
alias OpenApiSpex.Cast.Error
@dialyzer {:no_match, assert_schema: 3}
@doc """
Asserts that `value` conforms to the schema with title `schema_title` in `api_spec`.
"""
@spec assert_schema(map, String.t(), OpenApi.t()) :: map | no_return
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
- case Cast.cast(schema, value, api_spec.components.schemas) do
+ case OpenApiSpex.cast_value(value, schema, api_spec) do
{:ok, data} ->
data
{:error, errors} ->
errors =
Enum.map(errors, fn error ->
message = Error.message(error)
path = Error.path_to_string(error)
"#{message} at #{path}"
end)
flunk(
"Value does not conform to schema #{schema_title}: #{Enum.join(errors, "\n")}\n#{
inspect(value)
}"
)
end
end
end
diff --git a/test/cast/all_of_test.exs b/test/cast/all_of_test.exs
index 9bb9217..3c071e8 100644
--- a/test/cast/all_of_test.exs
+++ b/test/cast/all_of_test.exs
@@ -1,79 +1,79 @@
defmodule OpenApiSpex.CastAllOfTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, Schema}
alias OpenApiSpex.Cast.{Error, AllOf}
- alias OpenApiSpex.Test.Assertions2
+ alias OpenApiSpex.TestAssertions
defp cast(ctx), do: AllOf.cast(struct(Cast, ctx))
describe "cast/1" do
test "allOf" do
schema = %Schema{allOf: [%Schema{type: :integer}, %Schema{type: :string}]}
assert {:ok, 1} = cast(value: "1", schema: schema)
end
test "allOf, uncastable schema" do
schema = %Schema{allOf: [%Schema{type: :integer}, %Schema{type: :string}]}
assert {:error, [error]} = cast(value: [:whoops], schema: schema)
assert Error.message(error) ==
"Failed to cast value as string. Value must be castable using `allOf` schemas listed."
schema_with_title = %Schema{allOf: [%Schema{title: "Age", type: :integer}]}
assert {:error, [error_with_schema_title]} =
cast(value: [:nopes], schema: schema_with_title)
assert Error.message(error_with_schema_title) ==
"Failed to cast value as Age. Value must be castable using `allOf` schemas listed."
end
test "a more sophisticated example" do
dog = %{"bark" => "woof", "pet_type" => "Dog"}
- Assertions2.assert_schema(dog, "Dog", OpenApiSpexTest.ApiSpec.spec())
+ TestAssertions.assert_schema(dog, "Dog", OpenApiSpexTest.ApiSpec.spec())
end
test "allOf, for inheritance schema" do
schema = %Schema{
allOf: [
%Schema{
type: :object,
additionalProperties: true,
properties: %{
id: %Schema{
type: :string
}
}
},
%Schema{
type: :object,
additionalProperties: true,
properties: %{
bar: %Schema{
type: :string
}
}
}
]
}
value = %{id: "e30aee0f-dbda-40bd-9198-6cf609b8b640", bar: "foo"}
assert {:ok, %{id: "e30aee0f-dbda-40bd-9198-6cf609b8b640", bar: "foo"}} =
cast(value: value, schema: schema)
end
end
test "allOf, for multi-type array" do
schema = %Schema{
allOf: [
%Schema{type: :array, items: %Schema{type: :integer}},
%Schema{type: :array, items: %Schema{type: :boolean}},
%Schema{type: :array, items: %Schema{type: :string}}
]
}
value = ["Test #1", "2", "3", "4", "true", "Five!"]
assert {:ok, [2, 3, 4, true, "Test #1", "Five!"]} = cast(value: value, schema: schema)
end
end
diff --git a/test/cast/one_of_test.exs b/test/cast/one_of_test.exs
index 381e933..5883c85 100644
--- a/test/cast/one_of_test.exs
+++ b/test/cast/one_of_test.exs
@@ -1,37 +1,37 @@
defmodule OpenApiSpex.CastOneOfTest do
use ExUnit.Case
alias OpenApiSpex.{Cast, Schema}
alias OpenApiSpex.Cast.{Error, OneOf}
- alias OpenApiSpex.Test.Assertions2
+ alias OpenApiSpex.TestAssertions
defp cast(ctx), do: OneOf.cast(struct(Cast, ctx))
describe "cast/1" do
test "oneOf" do
schema = %Schema{oneOf: [%Schema{type: :integer}, %Schema{type: :string}]}
assert {:ok, "hello"} = cast(value: "hello", schema: schema)
end
test "oneOf, more than one matching schema" do
schema = %Schema{oneOf: [%Schema{type: :integer}, %Schema{type: :string}]}
assert {:error, [error]} = cast(value: "1", schema: schema)
assert error.reason == :one_of
assert Error.message(error) ==
"Failed to cast value to one of: Schema(type: :string), Schema(type: :integer)"
end
test "oneOf, no castable schema" do
schema = %Schema{oneOf: [%Schema{type: :string}]}
assert {:error, [error]} = cast(value: 1, schema: schema)
assert error.reason == :one_of
assert Error.message(error) == "Failed to cast value to one of: [] (no schemas provided)"
end
test "a more sophisticated case" do
dog = %{"bark" => "woof", "pet_type" => "Dog"}
- Assertions2.assert_schema(dog, "CatOrDog", OpenApiSpexTest.ApiSpec.spec())
+ TestAssertions.assert_schema(dog, "CatOrDog", OpenApiSpexTest.ApiSpec.spec())
end
end
end
diff --git a/test/plug/cast_and_validate/custom_error_user_controller_test.exs b/test/plug/cast_and_validate/custom_error_user_controller_test.exs
index 7824d2a..d4ca93e 100644
--- a/test/plug/cast_and_validate/custom_error_user_controller_test.exs
+++ b/test/plug/cast_and_validate/custom_error_user_controller_test.exs
@@ -1,39 +1,37 @@
defmodule OpenApiSpex.Plug.CastAndValidate.CustomErrorUserControllerTest do
use ExUnit.Case, async: true
describe "query params - param with custom error handling" do
test "Valid Param" do
conn =
:get
- |> Plug.Test.conn("/api/cast_and_validate_test/custom_error_users?validParam=true")
+ |> Plug.Test.conn("/api/custom_error_users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
@tag :capture_log
test "Invalid value" do
conn =
:get
- |> Plug.Test.conn("/api/cast_and_validate_test/custom_error_users?validParam=123")
+ |> Plug.Test.conn("/api/custom_error_users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
assert conn.resp_body == "Invalid boolean. Got: string"
end
@tag :capture_log
test "Invalid Param" do
conn =
:get
- |> Plug.Test.conn(
- "/api/cast_and_validate_test/custom_error_users?validParam=123&inValidParam=123&inValid2=hi"
- )
+ |> Plug.Test.conn("/api/custom_error_users?validParam=123&inValidParam=123&inValid2=hi")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 400
assert conn.resp_body == "Unexpected field: inValid2"
end
end
end
diff --git a/test/plug/cast_and_validate_test.exs b/test/plug/cast_and_validate_test.exs
index 98c4676..c2c5467 100644
--- a/test/plug/cast_and_validate_test.exs
+++ b/test/plug/cast_and_validate_test.exs
@@ -1,126 +1,159 @@
-defmodule OpenApiSpex.Plug.CastAndValidateTest do
- use ExUnit.Case, async: true
+defmodule OpenApiSpex.Plug.CastTest do
+ use ExUnit.Case
describe "query params - basics" do
test "Valid Param" do
conn =
:get
- |> Plug.Test.conn("/api/cast_and_validate_test/users?validParam=true")
+ |> Plug.Test.conn("/api/users?validParam=true")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
- @tag :capture_log
test "Invalid value" do
conn =
:get
- |> Plug.Test.conn("/api/cast_and_validate_test/users?validParam=123")
+ |> Plug.Test.conn("/api/users?validParam=123")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 422
end
- @tag :capture_log
test "Invalid Param" do
conn =
:get
- |> Plug.Test.conn(
- "/api/cast_and_validate_test/users?validParam=123&inValidParam=123&inValid2=hi"
- )
+ |> Plug.Test.conn("/api/users?validParam=123&inValidParam=123&inValid2=hi")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 422
+ error_resp = Jason.decode!(conn.resp_body)
- assert conn.resp_body ==
- "{\"errors\":[{\"message\":\"Unexpected field: inValid2\",\"source\":{\"pointer\":\"/inValid2\"},\"title\":\"Invalid value\"}]}"
+ assert error_resp == %{
+ "errors" => [
+ %{
+ "message" => "Unexpected field: inValid2",
+ "source" => %{"pointer" => "/inValid2"},
+ "title" => "Invalid value"
+ }
+ ]
+ }
end
- @tag :capture_log
test "with requestBody" do
body =
Jason.encode!(%{
phone_number: "123-456-789",
postal_address: "123 Lane St"
})
conn =
:post
- |> Plug.Test.conn("/api/cast_and_validate_test/users/123/contact_info", body)
+ |> Plug.Test.conn("/api/users/123/contact_info", body)
|> Plug.Conn.put_req_header("content-type", "application/json")
|> OpenApiSpexTest.Router.call([])
assert conn.status == 200
end
end
+ describe "query params - param with custom error handling" do
+ test "Valid Param" do
+ conn =
+ :get
+ |> Plug.Test.conn("/api/custom_error_users?validParam=true")
+ |> OpenApiSpexTest.Router.call([])
+
+ assert conn.status == 200
+ end
+
+ test "Invalid value" do
+ conn =
+ :get
+ |> Plug.Test.conn("/api/custom_error_users?validParam=123")
+ |> OpenApiSpexTest.Router.call([])
+
+ assert conn.status == 400
+ end
+
+ test "Invalid Param" do
+ conn =
+ :get
+ |> Plug.Test.conn("/api/custom_error_users?validParam=123&inValidParam=123&inValid2=hi")
+ |> OpenApiSpexTest.Router.call([])
+
+ assert conn.status == 400
+ assert conn.resp_body == "Unexpected field: inValid2"
+ end
+ end
+
describe "body params" do
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/cast_and_validate_test/users", Jason.encode!(request_body))
+ |> Plug.Test.conn("/api/users", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
|> OpenApiSpexTest.Router.call([])
assert conn.body_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 Jason.decode!(conn.resp_body) == %{
"data" => %{
"email" => "foo@bar.com",
"id" => 1234,
"inserted_at" => nil,
"name" => "asdf",
"updated_at" => "2017-09-12T14:44:55Z"
}
}
end
- @tag :capture_log
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/cast_and_validate_test/users", Jason.encode!(request_body))
+ |> Plug.Test.conn("/api/users", Jason.encode!(request_body))
|> Plug.Conn.put_req_header("content-type", "application/json")
conn = OpenApiSpexTest.Router.call(conn, [])
assert conn.status == 422
- resp_body = Jason.decode!(conn.resp_body)
-
- assert resp_body == %{
- "errors" => [
- %{
- "message" => "Invalid format. Expected ~r/[a-zA-Z][a-zA-Z0-9_]+/",
- "source" => %{"pointer" => "/user/name"},
- "title" => "Invalid value"
- }
- ]
- }
+ resp_data = Jason.decode!(conn.resp_body)
+
+ assert resp_data ==
+ %{
+ "errors" => [
+ %{
+ "message" => "Invalid format. Expected ~r/[a-zA-Z][a-zA-Z0-9_]+/",
+ "source" => %{"pointer" => "/user/name"},
+ "title" => "Invalid value"
+ }
+ ]
+ }
end
end
end
diff --git a/test/plug/cast_test.exs b/test/plug/cast_test.exs
deleted file mode 100644
index b30f42d..0000000
--- a/test/plug/cast_test.exs
+++ /dev/null
@@ -1,139 +0,0 @@
-defmodule OpenApiSpex.Plug.CastTest do
- use ExUnit.Case
-
- describe "query params - basics" do
- test "Valid Param" do
- conn =
- :get
- |> Plug.Test.conn("/api/users?validParam=true")
- |> OpenApiSpexTest.Router.call([])
-
- assert conn.status == 200
- end
-
- test "Invalid value" do
- conn =
- :get
- |> Plug.Test.conn("/api/users?validParam=123")
- |> OpenApiSpexTest.Router.call([])
-
- assert conn.status == 422
- end
-
- test "Invalid Param" do
- conn =
- :get
- |> Plug.Test.conn("/api/users?validParam=123&inValidParam=123&inValid2=hi")
- |> OpenApiSpexTest.Router.call([])
-
- assert conn.status == 422
- assert conn.resp_body == "Undefined query parameter: \"inValid2\""
- end
-
- test "with requestBody" do
- body =
- Jason.encode!(%{
- phone_number: "123-456-789",
- postal_address: "123 Lane St"
- })
-
- conn =
- :post
- |> Plug.Test.conn("/api/users/123/contact_info", body)
- |> Plug.Conn.put_req_header("content-type", "application/json")
- |> OpenApiSpexTest.Router.call([])
-
- assert conn.status == 200
- end
- end
-
- describe "query params - param with custom error handling" do
- test "Valid Param" do
- conn =
- :get
- |> Plug.Test.conn("/api/custom_error_users?validParam=true")
- |> OpenApiSpexTest.Router.call([])
-
- assert conn.status == 200
- end
-
- test "Invalid value" do
- conn =
- :get
- |> Plug.Test.conn("/api/custom_error_users?validParam=123")
- |> OpenApiSpexTest.Router.call([])
-
- assert conn.status == 400
- end
-
- test "Invalid Param" do
- conn =
- :get
- |> Plug.Test.conn("/api/custom_error_users?validParam=123&inValidParam=123&inValid2=hi")
- |> OpenApiSpexTest.Router.call([])
-
- assert conn.status == 400
- assert conn.resp_body == "Undefined query parameter: \"inValid2\""
- end
- end
-
- describe "body params" do
- 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", Jason.encode!(request_body))
- |> Plug.Conn.put_req_header("content-type", "application/json; charset=UTF-8")
- |> OpenApiSpexTest.Router.call([])
-
- assert conn.body_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 Jason.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", Jason.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 ==
- "#/user/name: Value \"*1234\" does not match pattern: [a-zA-Z][a-zA-Z0-9_]+"
- end
- end
-end
diff --git a/test/schema_test.exs b/test/schema_test.exs
index 8007155..e735eb2 100644
--- a/test/schema_test.exs
+++ b/test/schema_test.exs
@@ -1,474 +1,474 @@
defmodule OpenApiSpex.SchemaTest do
use ExUnit.Case
alias OpenApiSpex.Schema
alias OpenApiSpexTest.{ApiSpec, Schemas}
- import OpenApiSpex.Test.Assertions
+ import OpenApiSpex.TestAssertions
doctest Schema
describe "schema/1" do
test "EntityWithDict Schema example matches schema" do
api_spec = ApiSpec.spec()
assert_schema(Schemas.EntityWithDict.schema().example, "EntityWithDict", api_spec)
end
test "User Schema example matches schema" do
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
describe "cast/3" do
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
test "cast request schema with unexpected fields returns error" 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",
"unexpected_field" => "unexpected value"
}
}
assert {:error, _} = Schema.cast(user_request_schema, input, schemas)
end
test "Cast Cat from Pet schema" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
pet_schema = schemas["Pet"]
input = %{
"pet_type" => "Cat",
"meow" => "meow"
}
assert {:ok, %Schemas.Cat{meow: "meow", pet_type: "Cat"}} =
Schema.cast(pet_schema, input, schemas)
end
test "Cast Dog from oneOf [cat, dog] schema" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
cat_or_dog = Map.fetch!(schemas, "CatOrDog")
input = %{
"pet_type" => "Dog",
"bark" => "bowow"
}
assert {:ok, %Schemas.Dog{bark: "bowow", pet_type: "Dog"}} =
Schema.cast(cat_or_dog, input, schemas)
end
test "Cast Cat from oneOf [cat, dog] schema" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
cat_or_dog = Map.fetch!(schemas, "CatOrDog")
input = %{
"pet_type" => "Cat",
"meow" => "meow"
}
assert {:ok, %Schemas.Cat{meow: "meow", pet_type: "Cat"}} =
Schema.cast(cat_or_dog, input, schemas)
end
test "Cast number to string or number" do
schema = %Schema{
oneOf: [
%Schema{type: :number},
%Schema{type: :string}
]
}
result = Schema.cast(schema, "123", %{})
assert {:ok, 123.0} = result
end
test "Cast integer to float" do
schema = %Schema{type: :number}
assert Schema.cast(schema, 123, %{}) === {:ok, 123.0}
end
test "Cast string to oneOf number or date-time" do
schema = %Schema{
oneOf: [
%Schema{type: :number},
%Schema{type: :string, format: :"date-time"}
]
}
assert {:ok, %DateTime{}} = Schema.cast(schema, "2018-04-01T12:34:56Z", %{})
end
test "Cast string to anyOf number or date-time" do
schema = %Schema{
oneOf: [
%Schema{type: :number},
%Schema{type: :string, format: :"date-time"}
]
}
assert {:ok, %DateTime{}} = Schema.cast(schema, "2018-04-01T12:34:56Z", %{})
end
end
describe "Integer validation" do
test "Validate schema type integer when value is object" do
schema = %Schema{
type: :integer
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
end
describe "Number validation" do
test "Validate schema type number when value is object" do
schema = %Schema{
type: :integer
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
test "Validate schema type number when value is exclusive" do
schema = %Schema{
type: :integer,
minimum: -1,
maximum: 1,
exclusiveMinimum: true,
exclusiveMaximum: true
}
assert {:error, _} = Schema.validate(schema, 1, %{})
assert {:error, _} = Schema.validate(schema, -1, %{})
end
end
describe "String validation" do
test "Validate schema type string when value is object" do
schema = %Schema{
type: :string
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
test "Validate schema type string when value is DateTime" do
schema = %Schema{
type: :string
}
assert {:error, _} = Schema.validate(schema, DateTime.utc_now(), %{})
end
test "Validate non-empty string with expected value" do
schema = %Schema{type: :string, minLength: 1}
assert :ok = Schema.validate(schema, "BLIP", %{})
end
end
describe "DateTime validation" do
test "Validate schema type string with format date-time when value is DateTime" do
schema = %Schema{
type: :string,
format: :"date-time"
}
assert :ok = Schema.validate(schema, DateTime.utc_now(), %{})
end
end
describe "Date Validation" do
test "Validate schema type string with format date when value is Date" do
schema = %Schema{
type: :string,
format: :date
}
assert :ok = Schema.validate(schema, Date.utc_today(), %{})
end
end
describe "Enum validation" do
test "Validate string enum with unexpected value" do
schema = %Schema{
type: :string,
enum: ["foo", "bar"]
}
assert {:error, _} = Schema.validate(schema, "baz", %{})
end
test "Validate string enum with expected value" do
schema = %Schema{
type: :string,
enum: ["foo", "bar"]
}
assert :ok = Schema.validate(schema, "bar", %{})
end
end
describe "Object validation" do
test "Validate schema type object when value is array" do
schema = %Schema{
type: :object
}
assert {:error, _} = Schema.validate(schema, [], %{})
end
test "Validate schema type object when value is DateTime" do
schema = %Schema{
type: :object
}
assert {:error, _} = Schema.validate(schema, DateTime.utc_now(), %{})
end
end
describe "Array validation" do
test "Validate schema type array when value is object" do
schema = %Schema{
type: :array
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
end
describe "Boolean validation" do
test "Validate schema type boolean when value is object" do
schema = %Schema{
type: :boolean
}
assert {:error, _} = Schema.validate(schema, %{}, %{})
end
end
describe "AnyOf validation" do
test "Validate anyOf schema with valid value" do
schema = %Schema{
anyOf: [
%Schema{type: :array},
%Schema{type: :string}
]
}
assert :ok = Schema.validate(schema, "a string", %{})
end
test "Validate anyOf with value matching more than one schema" do
schema = %Schema{
anyOf: [
%Schema{type: :number},
%Schema{type: :integer}
]
}
assert :ok = Schema.validate(schema, 42, %{})
end
test "Validate anyOf schema with invalid value" do
schema = %Schema{
anyOf: [
%Schema{type: :string},
%Schema{type: :array}
]
}
assert {:error, _} = Schema.validate(schema, 3.14159, %{})
end
end
describe "OneOf validation" do
test "Validate oneOf schema with valid value" do
schema = %Schema{
oneOf: [
%Schema{type: :string},
%Schema{type: :array}
]
}
assert :ok = Schema.validate(schema, [1, 2, 3], %{})
end
test "Validate oneOf schema with invalid value" do
schema = %Schema{
oneOf: [
%Schema{type: :string},
%Schema{type: :array}
]
}
assert {:error, _} = Schema.validate(schema, 3.14159, %{})
end
test "Validate oneOf schema when matching multiple schemas" do
schema = %Schema{
oneOf: [
%Schema{type: :object, properties: %{a: %Schema{type: :string}}},
%Schema{type: :object, properties: %{b: %Schema{type: :string}}}
]
}
assert {:error, _} = Schema.validate(schema, %{a: "a", b: "b"}, %{})
end
end
describe "AllOf validation" do
test "Validate allOf schema with valid value" do
schema = %Schema{
allOf: [
%Schema{type: :object, properties: %{a: %Schema{type: :string}}},
%Schema{type: :object, properties: %{b: %Schema{type: :string}}}
]
}
assert :ok = Schema.validate(schema, %{a: "a", b: "b"}, %{})
end
test "Validate allOf schema with invalid value" do
schema = %Schema{
allOf: [
%Schema{type: :object, properties: %{a: %Schema{type: :string}}},
%Schema{type: :object, properties: %{b: %Schema{type: :string}}}
]
}
assert {:error, msg} = Schema.validate(schema, %{a: 1, b: 2}, %{})
assert msg =~ "#/a"
assert msg =~ "#/b"
end
test "Validate allOf with value matching not all schemas" do
schema = %Schema{
allOf: [
%Schema{
type: :integer,
minimum: 5
},
%Schema{
type: :integer,
maximum: 40
}
]
}
assert {:error, _} = Schema.validate(schema, 42, %{})
end
end
describe "Not validation" do
test "Validate not schema with valid value" do
schema = %Schema{
not: %Schema{type: :object}
}
assert :ok = Schema.validate(schema, 1, %{})
end
test "Validate not schema with invalid value" do
schema = %Schema{
not: %Schema{type: :object}
}
assert {:error, _} = Schema.validate(schema, %{a: 1}, %{})
end
test "Verify 'not' validation" do
schema = %Schema{not: %Schema{type: :boolean}}
assert :ok = Schema.validate(schema, 42, %{})
assert :ok = Schema.validate(schema, "42", %{})
assert :ok = Schema.validate(schema, nil, %{})
assert :ok = Schema.validate(schema, 4.2, %{})
assert :ok = Schema.validate(schema, [4], %{})
assert :ok = Schema.validate(schema, %{}, %{})
assert {:error, _} = Schema.validate(schema, true, %{})
assert {:error, _} = Schema.validate(schema, false, %{})
end
end
describe "Nullable validation" do
test "Validate nullable-ified with expected value" do
schema = %Schema{
nullable: true,
type: :string,
minLength: 1
}
assert :ok = Schema.validate(schema, "BLIP", %{})
end
test "Validate nullable with expected value" do
schema = %Schema{type: :string, nullable: true}
assert :ok = Schema.validate(schema, nil, %{})
end
test "Validate nullable with unexpected value" do
schema = %Schema{type: :string, nullable: true}
assert :ok = Schema.validate(schema, "bla", %{})
end
end
describe "Default property value" do
test "Available in structure" do
size = %Schemas.Size{}
assert "cm" == size.unit
assert 100 == size.value
end
test "Available after cast" do
api_spec = ApiSpec.spec()
schemas = api_spec.components.schemas
size = Map.fetch!(schemas, "Size")
assert {:ok, %Schemas.Size{value: 100, unit: "cm"}} ==
Schema.cast(size, %{}, schemas)
assert {:ok, %Schemas.Size{value: 110, unit: "cm"}} ==
Schema.cast(size, %{value: 110}, schemas)
end
end
end
diff --git a/test/server_test.exs b/test/server_test.exs
index d5555d6..bb611c0 100644
--- a/test/server_test.exs
+++ b/test/server_test.exs
@@ -1,41 +1,32 @@
defmodule OpenApiSpex.ServerTest do
use ExUnit.Case
alias OpenApiSpex.{Server}
alias OpenApiSpexTest.Endpoint
@otp_app :open_api_spex_test
describe "Server" do
test "from_endpoint/1" do
setup_endpoint()
server = Server.from_endpoint(Endpoint)
assert %{
- url: "https://example.com:1234/api/v1/"
- } = server
- end
-
- test "from_endpoint/2" do
- setup_endpoint()
-
- expected = Server.from_endpoint(Endpoint)
- actual = Server.from_endpoint(Endpoint, [opt_app: @otp_app])
-
- assert ^expected = actual
+ url: "https://example.com:1234/api/v1/"
+ } = server
end
end
defp setup_endpoint do
- Application.put_env(@otp_app, Endpoint, [
+ Application.put_env(@otp_app, Endpoint,
url: [
scheme: "https",
host: "example.com",
port: 1234,
path: "/api/v1/"
]
- ])
+ )
+
Endpoint.start_link()
end
end
-
diff --git a/test/support/cast_and_validate_user_controller.ex b/test/support/cast_and_validate_user_controller.ex
deleted file mode 100644
index 142bddd..0000000
--- a/test/support/cast_and_validate_user_controller.ex
+++ /dev/null
@@ -1,177 +0,0 @@
-defmodule OpenApiSpexTest.CastAndValidateUserController do
- use Phoenix.Controller
- alias OpenApiSpex.Operation
- alias OpenApiSpexTest.Schemas
-
- plug OpenApiSpex.Plug.CastAndValidate
-
- 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, %{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: [
- parameter(:validParam, :query, :boolean, "Valid Param", example: true)
- ],
- responses: %{
- 200 => response("User List Response", "application/json", Schemas.UsersResponse)
- }
- }
- end
-
- def index(conn, _params) do
- 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 = %{body_params: %Schemas.UserRequest{user: user = %Schemas.User{}}}, _) do
- json(conn, %Schemas.UserResponse{
- data: %{user | id: 1234}
- })
- end
-
- def contact_info_operation() do
- import Operation
-
- %Operation{
- tags: ["users"],
- summary: "Update contact info",
- description: "Update contact info",
- operationId: "UserController.contact_info",
- parameters: [
- parameter(:id, :path, :integer, "user ID")
- ],
- requestBody: request_body("Contact info", "application/json", Schemas.ContactInfo),
- responses: %{
- 200 => %OpenApiSpex.Response{
- description: "OK"
- }
- }
- }
- end
-
- # POST /contact_info
- def contact_info(conn = %{body_params: %Schemas.ContactInfo{}}, %{id: id}) do
- conn
- |> put_status(200)
- |> json(%{id: id})
- end
-
- def payment_details_operation() do
- import Operation
-
- %Operation{
- tags: ["users"],
- summary: "Show user payment details",
- description: "Shows a users payment details",
- operationId: "UserController.payment_details",
- parameters: [
- parameter(:id, :path, :integer, "User ID", example: 123, minimum: 1)
- ],
- responses: %{
- 200 => response("Payment Details", "application/json", Schemas.PaymentDetails)
- }
- }
- end
-
- def payment_details(conn, %{"id" => id}) do
- response =
- case rem(id, 2) do
- 0 ->
- %Schemas.CreditCardPaymentDetails{
- credit_card_number: "1234-5678-0987-6543",
- name_on_card: "Joe User",
- expiry: "0522"
- }
-
- 1 ->
- %Schemas.DirectDebitPaymentDetails{
- account_number: "98776543",
- account_name: "Joes Savings",
- bsb: "123-567"
- }
- end
-
- json(conn, response)
- end
-
- def create_entity_operation() do
- import Operation
-
- %Operation{
- tags: ["EntityWithDict"],
- summary: "Create an EntityWithDict",
- description: "Create an EntityWithDict",
- operationId: "UserController.create_entity",
- parameters: [],
- requestBody: request_body("Entity attributes", "application/json", Schemas.EntityWithDict),
- responses: %{
- 201 => response("EntityWithDict", "application/json", Schemas.EntityWithDict)
- }
- }
- end
-
- def create_entity(conn, %Schemas.EntityWithDict{} = entity) do
- json(conn, Map.put(entity, :id, 123))
- end
-end
diff --git a/test/support/custom_error_user_controller.ex b/test/support/custom_error_user_controller.ex
index 2acd746..c7df1da 100644
--- a/test/support/custom_error_user_controller.ex
+++ b/test/support/custom_error_user_controller.ex
@@ -1,56 +1,56 @@
defmodule OpenApiSpexTest.CustomErrorUserController do
use Phoenix.Controller
alias OpenApiSpex.Operation
alias OpenApiSpexTest.Schemas
alias Plug.Conn
defmodule CustomRenderErrorPlug do
-
@behaviour Plug
alias Plug.Conn
@impl Plug
def init(opts), do: opts
@impl Plug
- def call(conn, reason) do
- conn |> Conn.send_resp(400, "#{reason}")
+ def call(conn, errors) do
+ errors = errors |> Enum.map(&OpenApiSpex.error_message/1) |> Enum.join(",")
+ conn |> Conn.send_resp(400, "#{errors}")
end
end
- plug OpenApiSpex.Plug.Cast, render_error: CustomRenderErrorPlug
- plug OpenApiSpex.Plug.Validate, render_error: CustomRenderErrorPlug
+ plug OpenApiSpex.Plug.CastAndValidate, render_error: CustomRenderErrorPlug
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
def index_operation() do
import Operation
+
%Operation{
tags: ["users"],
summary: "List users",
description: "List all useres",
operationId: "UserController.index",
parameters: [
parameter(:validParam, :query, :boolean, "Valid Param", example: true)
],
responses: %{
200 => response("User List Response", "application/json", Schemas.UsersResponse)
}
}
end
+
def index(conn, _params) do
json(conn, %Schemas.UsersResponse{
data: [
%Schemas.User{
id: 123,
name: "joe user",
email: "joe@gmail.com"
}
]
})
end
-
end
diff --git a/test/support/router.ex b/test/support/router.ex
index 98ac480..97b6a21 100644
--- a/test/support/router.ex
+++ b/test/support/router.ex
@@ -1,34 +1,24 @@
defmodule OpenApiSpexTest.Router do
use Phoenix.Router
alias Plug.Parsers
alias OpenApiSpex.Plug.{PutApiSpec, RenderSpec}
pipeline :api do
plug :accepts, ["json"]
plug PutApiSpec, module: OpenApiSpexTest.ApiSpec
plug Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Jason
end
scope "/api", OpenApiSpexTest do
pipe_through :api
resources "/users", UserController, only: [:create, :index, :show]
# Used by ParamsTest
resources "/custom_error_users", CustomErrorUserController, only: [:index]
- # Used by CastAndValidateTest
- scope "/cast_and_validate_test" do
- get "/users", CastAndValidateUserController, :index
- get "/users/:id", CastAndValidateUserController, :show
- post "/users", CastAndValidateUserController, :create
- post "/users/:id/contact_info", CastAndValidateUserController, :contact_info
- post "/users/:id/payment_details", CastAndValidateUserController, :payment_details
- resources "/custom_error_users", CastAndValidate.CustomErrorUserController, only: [:index]
- end
-
get "/users/:id/payment_details", UserController, :payment_details
post "/users/:id/contact_info", UserController, :contact_info
post "/users/create_entity", UserController, :create_entity
get "/openapi", RenderSpec, []
end
end
diff --git a/test/support/user_controller.ex b/test/support/user_controller.ex
index 1c48db5..561daa3 100644
--- a/test/support/user_controller.ex
+++ b/test/support/user_controller.ex
@@ -1,164 +1,176 @@
defmodule OpenApiSpexTest.UserController do
use Phoenix.Controller
alias OpenApiSpex.Operation
alias OpenApiSpexTest.Schemas
- plug OpenApiSpex.Plug.Cast
- plug OpenApiSpex.Plug.Validate
+ plug OpenApiSpex.Plug.CastAndValidate
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, %{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: [
parameter(:validParam, :query, :boolean, "Valid Param", example: true)
],
responses: %{
200 => response("User List Response", "application/json", Schemas.UsersResponse)
}
}
end
+
def index(conn, _params) do
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 = %{body_params: %Schemas.UserRequest{user: user = %Schemas.User{}}}, _) do
json(conn, %Schemas.UserResponse{
data: %{user | id: 1234}
})
end
def contact_info_operation() do
import Operation
+
%Operation{
tags: ["users"],
summary: "Update contact info",
description: "Update contact info",
operationId: "UserController.contact_info",
parameters: [
parameter(:id, :path, :integer, "user ID")
],
requestBody: request_body("Contact info", "application/json", Schemas.ContactInfo),
responses: %{
200 => %OpenApiSpex.Response{
description: "OK"
}
}
}
end
+
def contact_info(conn = %{body_params: %Schemas.ContactInfo{}}, %{id: id}) do
conn
|> put_status(200)
|> json(%{id: id})
end
def payment_details_operation() do
import Operation
+
%Operation{
tags: ["users"],
summary: "Show user payment details",
description: "Shows a users payment details",
operationId: "UserController.payment_details",
parameters: [
parameter(:id, :path, :integer, "User ID", example: 123, minimum: 1)
],
responses: %{
200 => response("Payment Details", "application/json", Schemas.PaymentDetails)
}
}
end
+
def payment_details(conn, %{"id" => id}) do
response =
case rem(id, 2) do
0 ->
%Schemas.CreditCardPaymentDetails{
credit_card_number: "1234-5678-0987-6543",
name_on_card: "Joe User",
expiry: "0522"
}
+
1 ->
%Schemas.DirectDebitPaymentDetails{
account_number: "98776543",
account_name: "Joes Savings",
bsb: "123-567"
}
end
- json(conn, response)
+ json(conn, response)
end
def create_entity_operation() do
import Operation
+
%Operation{
tags: ["EntityWithDict"],
summary: "Create an EntityWithDict",
description: "Create an EntityWithDict",
operationId: "UserController.create_entity",
parameters: [],
requestBody: request_body("Entity attributes", "application/json", Schemas.EntityWithDict),
responses: %{
201 => response("EntityWithDict", "application/json", Schemas.EntityWithDict)
}
}
end
+
def create_entity(conn, %Schemas.EntityWithDict{} = entity) do
json(conn, Map.put(entity, :id, 123))
end
end

File Metadata

Mime Type
text/x-diff
Expires
Wed, Nov 27, 5:54 PM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
40705
Default Alt Text
(83 KB)

Event Timeline