Page MenuHomePhorge

No OneTemporary

Size
22 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/open_api.ex b/lib/open_api_spex/open_api.ex
index f4fb87b..c371003 100644
--- a/lib/open_api_spex/open_api.ex
+++ b/lib/open_api_spex/open_api.ex
@@ -1,66 +1,66 @@
defmodule OpenApiSpex.OpenApi do
@moduledoc """
Defines the `OpenApiSpex.OpenApi.t` type.
"""
alias OpenApiSpex.{
Info, Server, Paths, Components,
SecurityRequirement, Tag, ExternalDocumentation,
OpenApi
}
@enforce_keys [:info, :paths]
defstruct [
openapi: "3.0.0",
info: nil,
servers: [],
paths: nil,
components: nil,
security: [],
tags: [],
externalDocs: nil
]
@typedoc """
[OpenAPI Object](https://swagger.io/specification/#oasObject)
This is the root document object of the OpenAPI document.
"""
@type t :: %OpenApi{
openapi: String.t,
info: Info.t,
servers: [Server.t],
paths: Paths.t,
components: Components.t | nil,
security: [SecurityRequirement.t],
tags: [Tag.t],
externalDocs: ExternalDocumentation.t | nil
}
defimpl Poison.Encoder do
def encode(api_spec = %OpenApi{}, options) do
api_spec
|> to_json()
|> Poison.Encoder.encode(options)
end
defp to_json(%Regex{source: source}), do: source
defp to_json(value = %{__struct__: _}) do
value
|> Map.from_struct()
|> to_json()
end
defp to_json(value) when is_map(value) do
value
|> Stream.map(fn {k,v} -> {to_string(k), to_json(v)} end)
|> Stream.filter(fn {_, nil} -> false; _ -> true end)
|> Enum.into(%{})
end
defp to_json(value) when is_list(value) do
Enum.map(value, &to_json/1)
end
defp to_json(nil), do: nil
defp to_json(true), do: true
defp to_json(false), do: false
defp to_json(value) when is_atom(value), do: to_string(value)
defp to_json(value), do: value
end
-end
\ No newline at end of file
+end
diff --git a/lib/open_api_spex/schema_resolver.ex b/lib/open_api_spex/schema_resolver.ex
index b0894e1..88257aa 100644
--- a/lib/open_api_spex/schema_resolver.ex
+++ b/lib/open_api_spex/schema_resolver.ex
@@ -1,176 +1,179 @@
defmodule OpenApiSpex.SchemaResolver do
@moduledoc """
Internal module used to resolve `OpenApiSpex.Schema` structs from atoms.
"""
alias OpenApiSpex.{
OpenApi,
Components,
PathItem,
Operation,
Parameter,
Reference,
MediaType,
Schema,
RequestBody,
Response
}
@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
components = spec.components || %Components{}
schemas = components.schemas || %{}
{paths, schemas} = resolve_schema_modules_from_paths(spec.paths, schemas)
schemas = resolve_schema_modules_from_schemas(schemas)
%{spec | paths: paths, components: %{components| schemas: schemas}}
end
defp resolve_schema_modules_from_paths(paths = %{}, schemas = %{}) do
Enum.reduce(paths, {paths, schemas}, fn {path, path_item}, {paths, schemas} ->
{new_path_item, schemas} = resolve_schema_modules_from_path_item(path_item, schemas)
{Map.put(paths, path, new_path_item), schemas}
end)
end
defp resolve_schema_modules_from_path_item(path = %PathItem{}, schemas) do
path
|> Map.from_struct()
|> Enum.filter(fn {_k, v} -> match?(%Operation{}, v) end)
|> Enum.reduce({path, schemas}, fn {k, operation}, {path, schemas} ->
{new_operation, schemas} = resolve_schema_modules_from_operation(operation, schemas)
{Map.put(path, k, new_operation), schemas}
end)
end
defp resolve_schema_modules_from_operation(operation = %Operation{}, schemas) do
{parameters, schemas} = resolve_schema_modules_from_parameters(operation.parameters, schemas)
{request_body, schemas} = resolve_schema_modules_from_request_body(operation.requestBody, schemas)
{responses, schemas} = resolve_schema_modules_from_responses(operation.responses, schemas)
new_operation = %{operation | parameters: parameters, requestBody: request_body, responses: responses}
{new_operation, schemas}
end
defp resolve_schema_modules_from_parameters(nil, schemas), do: {nil, schemas}
defp resolve_schema_modules_from_parameters(parameters, schemas) do
{parameters, schemas} =
Enum.reduce(parameters, {[], schemas}, fn parameter, {parameters, schemas} ->
{new_parameter, schemas} = resolve_schema_modules_from_parameter(parameter, schemas)
{[new_parameter | parameters], schemas}
end)
{Enum.reverse(parameters), schemas}
end
defp resolve_schema_modules_from_parameter(parameter = %Parameter{schema: schema, content: nil}, schemas) when is_atom(schema) do
{ref, new_schemas} = resolve_schema_modules_from_schema(schema, schemas)
new_parameter = %{parameter | schema: ref}
{new_parameter, new_schemas}
end
defp resolve_schema_modules_from_parameter(parameter = %Parameter{schema: nil, content: content = %{}}, schemas) do
{new_content, schemas} = resolve_schema_modules_from_content(content, schemas)
{%{parameter | content: new_content}, schemas}
end
defp resolve_schema_modules_from_parameter(parameter = %Parameter{}, schemas) do
{parameter, schemas}
end
defp resolve_schema_modules_from_content(nil, schemas), do: {nil, schemas}
defp resolve_schema_modules_from_content(content, schemas) do
Enum.reduce(content, {content, schemas}, fn {mime, media}, {content, schemas} ->
{new_media, schemas} = resolve_schema_modules_from_media_type(media, schemas)
{Map.put(content, mime, new_media), schemas}
end)
end
defp resolve_schema_modules_from_media_type(media = %MediaType{schema: schema}, schemas) when is_atom(schema) do
{ref, new_schemas} = resolve_schema_modules_from_schema(schema, schemas)
new_media = %{media | schema: ref}
{new_media, new_schemas}
end
defp resolve_schema_modules_from_media_type(media = %MediaType{}, schemas) do
{media, schemas}
end
defp resolve_schema_modules_from_request_body(nil, schemas), do: {nil, schemas}
defp resolve_schema_modules_from_request_body(request_body = %RequestBody{}, schemas) do
{content, schemas} = resolve_schema_modules_from_content(request_body.content, schemas)
new_request_body = %{request_body | content: content}
{new_request_body, schemas}
end
defp resolve_schema_modules_from_responses(responses = %{}, schemas = %{}) do
Enum.reduce(responses, {responses, schemas}, fn {status, response}, {responses, schemas} ->
{new_response, schemas} = resolve_schema_modules_from_response(response, schemas)
{Map.put(responses, status, new_response), schemas}
end)
end
defp resolve_schema_modules_from_response(response = %Response{}, schemas = %{}) do
{content, schemas} = resolve_schema_modules_from_content(response.content, schemas)
new_response = %{response | content: content}
{new_response, schemas}
end
defp resolve_schema_modules_from_schemas(schemas = %{}) do
Enum.reduce(schemas, schemas, fn {name, schema}, schemas ->
{schema, schemas} = resolve_schema_modules_from_schema(schema, schemas)
Map.put(schemas, name, schema)
end)
end
defp resolve_schema_modules_from_schema(false, schemas), do: {false, schemas}
defp resolve_schema_modules_from_schema(true, schemas), do: {true, schemas}
defp resolve_schema_modules_from_schema(nil, schemas), do: {nil, schemas}
+ defp resolve_schema_modules_from_schema(schema_list, schemas) when is_list(schema_list) do
+ Enum.map_reduce(schema_list, schemas, &resolve_schema_modules_from_schema/2)
+ end
defp resolve_schema_modules_from_schema(schema, schemas) when is_atom(schema) do
title = schema.schema().title
new_schemas =
if Map.has_key?(schemas, title) do
schemas
else
{new_schema, schemas} = resolve_schema_modules_from_schema(schema.schema(), schemas)
Map.put(schemas, title, new_schema)
end
{%Reference{"$ref": "#/components/schemas/#{title}"}, new_schemas}
end
defp resolve_schema_modules_from_schema(schema = %Schema{}, schemas) do
{all_of, schemas} = resolve_schema_modules_from_schema(schema.allOf, schemas)
{one_of, schemas} = resolve_schema_modules_from_schema(schema.oneOf, schemas)
{any_of, schemas} = resolve_schema_modules_from_schema(schema.anyOf, schemas)
{not_schema, schemas} = resolve_schema_modules_from_schema(schema.not, schemas)
{items, schemas} = resolve_schema_modules_from_schema(schema.items, schemas)
{additional, schemas} = resolve_schema_modules_from_schema(schema.additionalProperties, schemas)
{properties, schemas} = resolve_schema_modules_from_schema_properties(schema.properties, schemas)
schema =
%{schema |
allOf: all_of,
oneOf: one_of,
anyOf: any_of,
not: not_schema,
items: items,
additionalProperties: additional,
properties: properties
}
{schema, schemas}
end
defp resolve_schema_modules_from_schema(ref = %Reference{}, schemas), do: {ref, schemas}
defp resolve_schema_modules_from_schema_properties(nil, schemas), do: {nil, schemas}
defp resolve_schema_modules_from_schema_properties(properties, schemas) do
Enum.reduce(properties, {properties, schemas}, fn {name, property}, {properties, schemas} ->
{new_property, schemas} = resolve_schema_modules_from_schema(property, schemas)
{Map.put(properties, name, new_property), schemas}
end)
end
-end
\ No newline at end of file
+end
diff --git a/test/doc_test.exs b/test/doc_test.exs
index 2f901ae..5767ce9 100644
--- a/test/doc_test.exs
+++ b/test/doc_test.exs
@@ -1,5 +1,5 @@
defmodule OpenApiSpex.DocTest do
use ExUnit.Case, async: true
doctest OpenApiSpex.Reference
-end
\ No newline at end of file
+end
diff --git a/test/schema_resolver_test.exs b/test/schema_resolver_test.exs
index 9d23375..e639793 100644
--- a/test/schema_resolver_test.exs
+++ b/test/schema_resolver_test.exs
@@ -1,70 +1,92 @@
defmodule OpenApiSpex.SchemaResolverTest do
use ExUnit.Case
+
alias OpenApiSpex.{
- MediaType,
- OpenApi,
- Operation,
- PathItem,
- Reference,
- RequestBody,
- Response,
- Schema
+ Info,
+ MediaType,
+ OpenApi,
+ Operation,
+ PathItem,
+ Reference,
+ RequestBody,
+ Response,
+ Schema
}
test "Resolves schemas in OpenApi spec" do
spec = %OpenApi{
+ info: %Info{
+ title: "Test",
+ version: "1.0.0"
+ },
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
}
}
}
}
}
+ },
+ "/api/users/{id}/payment_details" => %PathItem{
+ get: %Operation{
+ responses: %{
+ 200 => %Response{
+ content: %{
+ "application/json" => %MediaType{
+ schema: OpenApiSpexTest.Schemas.PaymentDetails
+ }
+ }
+ }
+ }
+ }
}
}
}
resolved = OpenApiSpex.resolve_schema_modules(spec)
assert %Reference{"$ref": "#/components/schemas/UsersResponse"} =
- resolved.paths["/api/users"].get.responses[200].content["application/json"].schema
+ 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
+ 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
+ resolved.paths["/api/users"].post.requestBody.content["application/json"].schema
assert %{
- "UserRequest" => %Schema{},
- "UserResponse" => %Schema{},
- "User" => %Schema{},
- } = resolved.components.schemas
+ "UserRequest" => %Schema{},
+ "UserResponse" => %Schema{},
+ "User" => %Schema{},
+ "PaymentDetails" => %Schema{},
+ "CreditCardPaymentDetails" => %Schema{},
+ "DirectDebitPaymentDetails" => %Schema{}
+ } = resolved.components.schemas
end
-end
\ No newline at end of file
+end
diff --git a/test/support/router.ex b/test/support/router.ex
index 947fc6a..c2320b4 100644
--- a/test/support/router.ex
+++ b/test/support/router.ex
@@ -1,18 +1,19 @@
defmodule OpenApiSpexTest.Router do
use Phoenix.Router
alias Plug.Parsers
alias OpenApiSpexTest.UserController
alias OpenApiSpex.Plug.{PutApiSpec, RenderSpec}
pipeline :api do
plug :accepts, ["json"]
plug PutApiSpec, module: OpenApiSpexTest.ApiSpec
plug Parsers, parsers: [:json], pass: ["text/*"], json_decoder: Poison
end
scope "/api" do
pipe_through :api
resources "/users", UserController, only: [:create, :index, :show]
+ get "/users/:id/payment_details", UserController, :payment_details
get "/openapi", RenderSpec, []
end
-end
\ No newline at end of file
+end
diff --git a/test/support/schemas.ex b/test/support/schemas.ex
index 6c3b6ac..341c918 100644
--- a/test/support/schemas.ex
+++ b/test/support/schemas.ex
@@ -1,90 +1,140 @@
defmodule OpenApiSpexTest.Schemas do
require OpenApiSpex
alias OpenApiSpex.Schema
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
+ defmodule CreditCardPaymentDetails do
+ OpenApiSpex.schema %{
+ title: "CreditCardPaymentDetails",
+ description: "Payment details when using credit-card method",
+ type: :object,
+ properties: %{
+ credit_card_number: %Schema{type: :string, description: "Credit card number"},
+ name_on_card: %Schema{type: :string, description: "Name as appears on card"},
+ expiry: %Schema{type: :string, description: "4 digit expiry MMYY"}
+ },
+ required: [:credit_card_number, :name_on_card, :expiry],
+ example: %{
+ "credit_card_number" => "1234-5678-1234-6789",
+ "name_on_card" => "Joe User",
+ "expiry" => "1234"
+ }
+ }
+ end
+
+ defmodule DirectDebitPaymentDetails do
+ OpenApiSpex.schema %{
+ title: "DirectDebitPaymentDetails",
+ description: "Payment details when using direct-debit method",
+ type: :object,
+ properties: %{
+ account_number: %Schema{type: :string, description: "Bank account number"},
+ account_name: %Schema{type: :string, description: "Name of account"},
+ bsb: %Schema{type: :string, description: "Branch identifier"}
+ },
+ required: [:account_number, :account_name, :bsb],
+ example: %{
+ "account_number" => "12349876",
+ "account_name" => "Joes Savings Account",
+ "bsb" => "123-4567"
+ }
+ }
+ end
+
+ defmodule PaymentDetails do
+ OpenApiSpex.schema %{
+ title: "PaymentDetails",
+ description: "Abstract Payment details type",
+ type: :object,
+ oneOf: [
+ CreditCardPaymentDetails,
+ DirectDebitPaymentDetails
+ ]
+ }
+ end
+
defmodule UserRequest do
OpenApiSpex.schema %{
title: "UserRequest",
description: "POST body for creating a user",
type: :object,
properties: %{
user: User
},
example: %{
"user" => %{
"name" => "Joe User",
"email" => "joe@gmail.com"
}
}
}
end
defmodule UserResponse do
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",
"inserted_at" => "2017-09-12T12:34:55Z",
"updated_at" => "2017-09-13T10:11:12Z"
}
},
"x-struct": __MODULE__
}
end
defmodule UsersResponse do
OpenApiSpex.schema %{
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"
}
]
}
}
end
-end
\ No newline at end of file
+end
diff --git a/test/support/user_controller.ex b/test/support/user_controller.ex
index 8d22c2f..382a290 100644
--- a/test/support/user_controller.ex
+++ b/test/support/user_controller.ex
@@ -1,85 +1,120 @@
defmodule OpenApiSpexTest.UserController do
use Phoenix.Controller
alias OpenApiSpex.Operation
alias OpenApiSpexTest.Schemas
plug OpenApiSpex.Plug.Cast
plug OpenApiSpex.Plug.Validate
def open_api_operation(action) do
apply(__MODULE__, :"#{action}_operation", [])
end
@doc """
API Spec for :show action
"""
def show_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "Show user",
description: "Show a user by ID",
operationId: "UserController.show",
parameters: [
parameter(:id, :path, :integer, "User ID", example: 123, minimum: 1)
],
responses: %{
200 => response("User", "application/json", Schemas.UserResponse)
}
}
end
def show(conn, %{id: id}) do
json(conn, %Schemas.UserResponse{
data: %Schemas.User{
id: id,
name: "joe user",
email: "joe@gmail.com"
}
})
end
def index_operation() do
import Operation
%Operation{
tags: ["users"],
summary: "List users",
description: "List all useres",
operationId: "UserController.index",
parameters: [],
responses: %{
200 => response("User List Response", "application/json", Schemas.UsersResponse)
}
}
end
def index(conn, _params) do
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, %Schemas.UserRequest{user: user = %Schemas.User{}}) do
json(conn, %Schemas.UserResponse{
data: %{user | id: 1234}
})
end
-end
\ No newline at end of file
+
+ 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
+end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 19, 3:05 AM (21 h, 28 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1765414
Default Alt Text
(22 KB)

Event Timeline