Current section

Files

Jump to
xcribe lib swagger formatter.ex
Raw

lib/swagger/formatter.ex

defmodule Xcribe.Swagger.Formatter do
@moduledoc ~S"""
Format a given `Xcribe.Request` according to OpenAPI Specification.
To know more about the specifications [OpenAPI 3.0.3](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md)
"""
alias Xcribe.{ContentDecoder, Request}
alias Xcribe.Swagger.Types
import Xcribe.Helpers.Formatter, only: [content_type: 1, authorization: 1]
@doc """
Return an empty struct of an OpenAPI Object.
"""
def raw_openapi_object do
%{
openapi: "3.0.3",
info: nil,
servers: nil,
paths: nil,
components: nil
}
end
@doc """
Return an Info Object builded from the api_info suplied by the `Xcribe.Information`.
"""
def info_object(api_info) do
%{title: api_info.name, description: api_info.description, version: "1"}
end
@doc """
Return a Server Object builded from the api_info suplied by the `Xcribe.Information`.
"""
def server_object(api_info) do
[%{url: api_info.host, description: ""}]
end
@doc """
Return a Path Item Object from the given request.
"""
def path_item_object_from_request(%Request{verb: verb} = request) do
%{
verb =>
path_item_object_add_request_body(
request,
%{
description: "",
summary: "",
responses: responses_object_from_request(request),
parameters: parameter_objects_from_request(request),
security: security_requirement_object_by_request(request)
}
)
}
end
@doc """
Return a Request Body Object from given request
"""
def request_body_object_from_request(%Request{header_params: headers, request_body: body}) do
media_type_object(headers, body)
end
@doc """
Return a Response Object from given request
"""
def response_object_from_request(%Request{resp_headers: headers, resp_body: body}) do
headers
|> media_type_object(body)
|> response_object_add_headers(headers)
end
@doc """
Return a list of Parameter Objects from a given request.
"""
def parameter_objects_from_request(%Request{} = request) do
path_list(request) ++ header_list(request) ++ query_list(request)
end
@doc """
Return the security requirement for given request.
"""
def security_requirement_object_by_request(%Request{header_params: headers}) do
case authorization(headers) do
nil -> []
auth -> [%{security_type(auth) => []}]
end
end
@doc """
Return the Security Scheme Object for given request.
"""
def security_scheme_object_from_request(%Request{header_params: headers}) do
case authorization(headers) do
nil -> %{}
auth -> auth |> security_type() |> security_scheme_by_type()
end
end
@opt_no_title {:title, false}
@opt_example {:example, true}
@doc ~S"""
Return an schema object for given attribute/parameter.
### Options:
* `:title` - Include the schema title, default is `true`.
* `:example` - Include the schema example, default is `false`.
"""
def schema_object_for(param, opts \\ [])
def schema_object_for({title, value}, opts) when is_map(value) do
%{type: "object"}
|> schema_add_title(title, @opt_no_title in opts)
|> schema_add_properties(value, opts)
end
def schema_object_for({title, value}, opts) when is_list(value) do
%{type: "array"}
|> schema_add_title(title, @opt_no_title in opts)
|> schema_add_items(value, opts)
end
def schema_object_for({title, value}, opts) do
%{type: Types.type_for(value)}
|> schema_add_title(title, @opt_no_title in opts)
|> schema_add_format(Types.format_for(value))
|> schema_add_example(value, @opt_example in opts)
end
@doc """
Merge two lists of parameter object keep uniq names
"""
def merge_parameter_object_lists(base_list, new_list) do
Enum.reduce(new_list, base_list, &merge_parameter_func/2)
end
@doc """
Merge two path item objects
"""
def merge_path_item_objects(item_one, item_two, verb) do
Map.update(
item_one,
verb,
item_two[verb],
&merge_path_items(&1, item_two[verb])
)
end
defp merge_path_items(item_one, %{parameters: params, responses: resp} = item_two) do
item_one
|> Map.update(:parameters, params, &merge_parameter_object_lists(&1, params))
|> Map.update(:responses, resp, &Map.merge(&1, resp))
|> merge_request_body_if_needed(item_two)
end
defp merge_request_body_if_needed(%{requestBody: _} = item, %{requestBody: new_body}) do
Map.update(item, :requestBody, new_body, &merge_request_body(&1, new_body))
end
defp merge_request_body_if_needed(item, %{requestBody: body}),
do: Map.put(item, :requestBody, body)
defp merge_request_body_if_needed(item, _), do: item
defp merge_request_body(body, new_body) do
%{description: "", content: Map.merge(body.content, new_body.content)}
end
defp merge_parameter_func(param, params) do
if has_param?(param, params), do: params, else: [param | params]
end
defp has_param?(param, params), do: Enum.any?(params, &eql_name_and_in(&1, param))
defp eql_name_and_in(%{name: name, in: inn}, %{name: name, in: inn}), do: true
defp eql_name_and_in(_base_param, _new_param), do: false
defp path_item_object_add_request_body(%{request_body: body}, path_item_object)
when body == %{},
do: path_item_object
defp path_item_object_add_request_body(request, path_item_object) do
Map.put(
path_item_object,
:requestBody,
request_body_object_from_request(request)
)
end
defp responses_object_from_request(%Request{status_code: status} = request) do
%{status => response_object_from_request(request)}
end
defp media_type_object(_headers, ""), do: %{description: ""}
defp media_type_object(headers, content) do
media_type = content_type(headers)
%{
description: "",
content: %{
media_type => %{schema: build_schema_for_media(content, media_type)}
}
}
end
defp build_schema_for_media(content, content_type) when is_binary(content) do
content
|> ContentDecoder.decode!(content_type)
|> build_schema_for_media(content_type)
end
defp build_schema_for_media(content, _) do
schema_object_for({:build_schema_for_media, content}, title: false, example: true)
end
defp response_object_add_headers(response_object, headers) do
Map.put(
response_object,
:headers,
Enum.reduce(headers, %{}, &reduce_header_objects/2)
)
end
defp reduce_header_objects({"content-type", _value}, headers), do: headers
defp reduce_header_objects({title, value}, headers) do
Map.put(
headers,
title,
%{
description: "",
schema: schema_object_for({title, value}, title: false)
}
)
end
defp header_list(%{header_params: params}),
do: Enum.reduce(params, [], &reduce_header_parameter/2)
defp path_list(%{path_params: params}), do: Enum.map(params, &parameter_object(&1, "path"))
defp query_list(%{query_params: params}), do: Enum.map(params, &parameter_object(&1, "query"))
defp reduce_header_parameter({"content-type", _value}, acc), do: acc
defp reduce_header_parameter({"authorization", _value}, acc), do: acc
defp reduce_header_parameter({"accept", _value}, acc), do: acc
defp reduce_header_parameter(param, acc), do: [parameter_object(param, "header") | acc]
defp parameter_object({name, value}, inn) do
parameter_object_add_required(%{
name: name,
in: inn,
schema: schema_object_for({:parameter_object, value}, title: false),
example: value
})
end
defp parameter_object_add_required(%{in: "path"} = param), do: Map.put(param, :required, true)
defp parameter_object_add_required(param), do: param
defp schema_add_title(schema, _title, true), do: schema
defp schema_add_title(schema, title, false), do: Map.put(schema, :title, title)
defp schema_add_format(schema, ""), do: schema
defp schema_add_format(schema, format), do: Map.put(schema, :format, format)
defp schema_add_example(schema, value, true), do: Map.put(schema, :example, value)
defp schema_add_example(schema, _value, false), do: schema
defp schema_add_items(schema, [], _opts) do
Map.put(schema, :items, %{type: "string"})
end
defp schema_add_items(schema, [value | _], opts) do
item_opts = Keyword.merge(opts, title: false)
Map.put(
schema,
:items,
schema_object_for({:schema_add_items, value}, item_opts)
)
end
defp schema_add_properties(schema, value, opts) do
Map.put(schema, :properties, reduce_properties(value, opts))
end
defp reduce_properties(properties, opts) do
property_opts = Keyword.merge(opts, title: false)
Enum.reduce(properties, %{}, fn {title, value}, schema ->
Map.put(
schema,
title,
schema_object_for({:schema_add_properties, value}, property_opts)
)
end)
end
defp security_type("Bearer" <> _tail), do: "bearer"
defp security_type("Basic" <> _tail), do: "basic"
defp security_type(_), do: "api_key"
defp security_scheme_by_type("api_key") do
%{
"api_key" => %{
"type" => "apiKey",
"name" => "authorization",
"in" => "header"
}
}
end
defp security_scheme_by_type("bearer") do
%{
"bearer" => %{
type: "http",
scheme: "bearer",
bearerFormat: "JWT"
}
}
end
defp security_scheme_by_type("basic") do
%{
"basic" => %{
type: "http",
scheme: "basic"
}
}
end
end