Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85710895
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
22 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 3:05 AM (19 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1765414
Default Alt Text
(22 KB)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment