Page MenuHomePhorge

No OneTemporary

Size
20 KB
Referenced Files
None
Subscribers
None
diff --git a/.travis.yml b/.travis.yml
index e4235ff..f927a0e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,18 +1,19 @@
language: elixir
elixir:
- 1.6.6
- 1.7.1
otp_release:
- 20.3
- 21.0
script:
- - mix test
- - mix dialyzer --halt-exit-status
+ - mix compile --warnings-as-errors
+ - mix test
+ - mix dialyzer --halt-exit-status
cache:
directories:
- _build
- - deps
\ No newline at end of file
+ - deps
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6ebbb1e..dac088d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,88 +1,111 @@
+# 3.0.0
+
+Major version bump as the behaviour of `OpenApiSpex.Plug.Cast` has changed (#39).
+To enable requests that contain a body as well as path or query params, the result of casting the
+request body is now placed in the `Conn.body_params` field, instead of the combined `Conn.params` field.
+
+This requires changing code such as Phoenix controller actions to from
+
+```elixir
+def create(conn, %UserRequest{user: %User{name: name, email: email}}) do
+```
+
+to
+
+```elixir
+ def create(conn = %{body_params: %UserRequest{user: %User{name: name, email: email}}}, params) do
+```
+
+- Feature: A custom plug may be provided to render errors (#46)
+- Fix compiler warnings and improve CI process (#53)
+- Fix: Support casting GET requests without Content-Type header (#50, #49)
+- Open API Spex has been moved to the new `open-api-spex` Github organisation
+
# 2.3.1
- Docs: Update example application to include swagger generate mix task (#41)
- Fix: Ignore charset in content-type header when looking up schema by content type. (#45)
Thanks to [dmt](https://github.com/dmt) and [fenollp](https://github.com/fenollp) for contributions!
# 2.3.0
- Feature: Validate string enum types. (#33)
- Feature: Detect and report missing API spec in `OpenApiSpex.Plug.Cast` (#37)
- Fix: Correct atom for parameter `style` field typespec (#36)
Thanks to [slavo2](https://github.com/slavo2) and [anagromataf](https://github.com/anagromataf) for
contributions!
# 2.2.0
- Feature: Support composite schemas in `OpenApiSpex.schema`
structs defined with `OpenApiSpex.schema` will include all properties defined in schemas
listed in `allOf`. See the `OpenApiSpex.Schema` docs for some examples.
- Feature: Support composite and polymorphic schemas with `OpenApiSpex.cast/3`.
- `discriminator` is used to cast polymorphic shemas to a more specific schema.
- `allOf` will cast all properties in each included schema
- `oneOf` / `anyOf` will attempt to use each schema until a successful cast is made
# 2.1.1
- Fix: (#24, #25) Operations that define `parameters` and a `requestBody` schema can be validated.
# 2.1.0
- Feature: (#16) Error response from `OpenApiSpex.cast` when value contains unknown properties and schema declares `additionalProperties: false`.
- Feature: (#20) Update swagger-ui to version 3.17.0.
- Fix: (#17, #21, #22) Update typespecs for struct types.
# 2.0.0
Major version update following from API change in `OpenApiSpex.cast` and `OpenApiSpex.validate`.
When casting/validating all parameters against an `OpenApiSpex.Operation`, the complete `Plug.Conn` struct must now be passed, where the combined params map was previously accepted.
This allows `OpenApiSpex.cast` / `OpenApiSpex.validate` to check that the parameters are being supplied in the expected location (query, path, body, header, cookie).
In version 2.0.0, only unexpected query parameters will cause a 422 response from `OpenApiSpex.Plug.Cast`, this may be extended in future versions to detect more invalid requests.
Thanks [cstaud](https://github.com/cstaud), [rutho](https://github.com/ThomasRueckert), [anagromataf](https://github.com/anagromataf) for contributions!
- Change: (#9) swagger-ui updated to 3.13.4
- Change (#9) Unexpected query parameters will produce an error response from `OpenApiSpex.Plug.Cast`
- Change: (#9) `OpenApiSpex.cast/4` now requires a complete `Plug.Conn` struct when casting all parameters of an `OpenApiSpex.Operation`
- Change: (#14) `OpenApiSpex.validate/4` now requires a complete `Plug.Conn` struct when validating all parameters of an `OpenApiSpex.Operation`
- Fix: (#11) Support resolving list of schema modules in `oneOf`, `anyOf`, etc.
- Fix: `OpenApiSpex.schema` macro allows defining schemas without any properties
- Fix: type declarations better reflect when `nil` is allowed
# 1.1.4
- `additionalProperties` is now `nil` by default, was previously `true`
# 1.1.3
- Fix several bugs and make some minor enhancements to schema casting and validating.
- Add sample application to enable end-to-end testing
# 1.1.2
Fix openapi version output in generated spec.
# 1.1.1
Update swagger-ui to version 3.3.2
# 1.1.0
Include path to invalid element in validation errors.
Eg: "#/user/name: Value does not match pattern: [a-zA-Z][a-zA-Z0-9_]+"
# 1.0.1
Cache API spec in application environment after first call to PutApiSpec plug
# 1.0.0
Initial release. This package is inspired by [phoenix_swagger](https://github.com/xerions/phoenix_swagger) but targets Open API Spec 3.0.
diff --git a/README.md b/README.md
index 9646a2d..5e73f22 100644
--- a/README.md
+++ b/README.md
@@ -1,299 +1,299 @@
# Open API Spex
-[![Build Status](https://travis-ci.org/mbuhot/open_api_spex.svg?branch=master)](https://travis-ci.org/mbuhot/open_api_spex)
+[![Build Status](https://travis-ci.org/open-api-spex/open_api_spex.svg?branch=master)](https://travis-ci.org/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, "~> 2.3"}
+ {:open_api_spex, "~> 3.0"}
]
end
```
## Generate Spec
Start by adding an `ApiSpec` module to your application to populate an `OpenApiSpex.OpenApi` struct.
```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. The callback populates an `OpenApiSpex.Operation` struct describing the plug/action.
```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", 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 implement the `OpenApiSpex.Schema` behaviour.
The only callback is `schema/0`, which should return an `OpenApiSpex.Schema` struct.
You may optionally declare a struct, linked to the JSON schema through the `x-struct` extension property.
See `OpenApiSpex.schema/1` macro for a convenient way to reduce some boilerplate.
```elixir
defmodule MyApp.Schemas do
alias OpenApiSpex.Schema
defmodule User do
@behaviour OpenApiSpex.Schema
@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"
}
"x-struct": __MODULE__
}
def schema, do: @schema
defstruct Map.keys(@schema.properties)
end
defmodule UserResponse do
require OpenApiSpex
# OpenApiSpex.schema/1 macro can be optionally used to reduce boilerplate code
OpenApiSpex.schema %{
title: "UserResponse",
description: "Response schema for single user",
type: :object,
properties: %{
data: User
}
}
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`
## Serve 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: 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
```
## Serve Swagger UI
Once your API spec is available through a route, 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
```
## Cast Params
Add the `OpenApiSpex.Plug.Cast` plug to a controller to cast the request parameters and body 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: [
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
```
See also `OpenApiSpex.cast/3` and `OpenApiSpex.Schema.cast/3` for more examples outside of a `plug` pipeline.
## Validate Params
Add both the `OpenApiSpex.Plug.Cast` and `OpenApiSpex.Plug.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.
The response body will include the validation error message:
```
#/user/name: Value does not match pattern: [a-zA-Z][a-zA-Z0-9_]+
```
See `OpenApiSpex.validate/3` and `OpenApiSpex.Schema.validate/3` for usage 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.
```elixir
Use ExUnit.Case
import OpenApiSpex.Test.Assertions
test "UsersResponse example matches schema" do
api_spec = MyApp.ApiSpec.spec()
schema = MyApp.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:
```elixir
use MyApp.ConnCase
import OpenApiSpex.Test.Assertions
test "UserController produces a UsersResponse", %{conn: conn} do
api_spec = MyApp.ApiSpec.spec()
json =
conn
|> get(user_path(conn, :index))
|> json_response(200)
assert_schema(json, "UsersResponse", api_spec)
end
```
diff --git a/mix.exs b/mix.exs
index 9621f6b..c00425f 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,58 +1,58 @@
defmodule OpenApiSpex.Mixfile do
use Mix.Project
- @version "2.3.1"
+ @version "3.0.0"
def project do
[
app: :open_api_spex,
version: @version,
- elixir: "~> 1.5",
+ elixir: "~> 1.6",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
description: description(),
package: package(),
deps: deps(),
- source_url: "https://github.com/mbuhot/open_api_spex",
- homepage_url: "https://github.com/mbuhot/open_api_spex",
+ source_url: "https://github.com/open-api-spex/open_api_spex",
+ homepage_url: "https://github.com/open-api-spex/open_api_spex",
docs: [extras: ["README.md"], main: "readme", source_ref: "v#{@version}"],
dialyzer: [
plt_add_apps: [:mix],
plt_add_deps: :apps_direct,
flags: ["-Werror_handling", "-Wno_unused", "-Wunmatched_returns", "-Wunderspecs"],
remove_defaults: [:unknown]
]
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Run "mix help compile.app" to learn about applications.
def application, do: [extra_applications: []]
defp description() do
"Leverage Open Api Specification 3 (swagger) to document, test, validate and explore your Plug and Phoenix APIs."
end
defp package() do
[
name: "open_api_spex",
files: ["lib", "mix.exs", "README.md", "LICENSE", "CHANGELOG.md"],
maintainers: ["Mike Buhot (m.buhot@gmail.com)"],
licenses: ["Mozilla Public License, version 2.0"],
- links: %{"GitHub" => "https://github.com/mbuhot/open_api_spex"}
+ links: %{"GitHub" => "https://github.com/open-api-spex/open_api_spex"}
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:poison, "~> 3.1"},
- {:plug, "~> 1.4"},
+ {:plug, "~> 1.7"},
{:phoenix, "~> 1.3", only: :test},
- {:ex_doc, "~> 0.16", only: :dev, runtime: false},
+ {:ex_doc, "~> 0.19", only: :dev, runtime: false},
{:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}
]
end
end
diff --git a/mix.lock b/mix.lock
index 11ed178..6dfbfe5 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,13 +1,14 @@
%{
"dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"},
- "ex_doc": {:hex, :ex_doc, "0.19.0", "e22b6434373b4870ea77b24df069dbac7002c1f483615e9ebfc0c37497e1c75c", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
+ "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "0.5.1", "966c5c2296da272d42f1de178c1d135e432662eca795d6dc12e5e8787514edf7", [:mix], [{:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.8.0", "1204a2f5b4f181775a0e456154830524cf2207cf4f9112215c05e0b76e4eca8b", [:mix], [{:makeup, "~> 0.5.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.2.2", "d526b23bdceb04c7ad15b33c57c4526bf5f50aaa70c7c141b4b4624555c68259", [:mix], [], "hexpm"},
- "phoenix": {:hex, :phoenix, "1.3.3", "bafb5fa408d202e8d9f739e781bdb908446a2c1c1e00797c1158918ed55566a4", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
+ "phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
- "plug": {:hex, :plug, "1.6.1", "c62fe7623d035020cf989820b38490460e6903ab7eee29e234b7586e9b6c91d6", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
+ "plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
+ "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Dec 1, 12:54 PM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41698
Default Alt Text
(20 KB)

Event Timeline