defmodule Xcribe.Swagger.Formatter do @moduledoc false alias Xcribe.{ContentDecoder, JsonSchema, Request} 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), tags: format_tags_from_resource(request.resource) } ) } 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 @doc """ Merge two lists of parameter object keep uniq names """ def merge_parameter_object_lists(base_list, new_list, mode \\ :keep) do new_list |> Enum.reduce(base_list, &merge_parameter_func(&1, &2, mode)) |> Enum.sort(&(&1.name < &2.name)) end @doc """ Merge two path item objects """ def merge_path_item_objects(base, new_item, verb) do Map.update( base, verb, new_item[verb], &merge_path_items(&1, new_item[verb]) ) end defp merge_path_items(base, %{parameters: params, responses: resp} = new_item) do mode = overwrite_mode(resp) base |> Map.update(:parameters, params, &merge_parameter_object_lists(&1, params, mode)) |> Map.update(:responses, resp, &Map.merge(&1, resp)) |> merge_request_body_if_needed(new_item, mode) end defp overwrite_mode(responses) do code = responses |> Map.keys() |> List.first() if code >= 200 and code < 300, do: :overwrite, else: :keep end defp merge_request_body_if_needed(%{requestBody: _body} = item, %{requestBody: new}, mode) do Map.update( item, :requestBody, new, &%{description: "", content: merge_request_body(&1, new, mode)} ) end defp merge_request_body_if_needed(item, %{requestBody: body}, _mode), do: Map.put(item, :requestBody, body) defp merge_request_body_if_needed(item, _new_item, _mode), do: item defp merge_request_body(body, new_body, :keep), do: Map.merge(new_body.content, body.content) defp merge_request_body(body, new_body, :overwrite), do: Map.merge(body.content, new_body.content) defp merge_parameter_func(new_param, params, :keep) do if has_param?(new_param, params), do: params, else: [new_param | params] end defp merge_parameter_func(new_param, params, :overwrite) do [new_param | drop_eql_param(new_param, params)] end defp drop_eql_param(param, params), do: Enum.reject(params, &eql_name_and_in(&1, param)) 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 format_tags_from_resource(resource), do: [String.replace(resource, "_", " ")] 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: JsonSchema.schema_for(content, title: false, example: true) 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: JsonSchema.schema_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, ¶meter_object(&1, "path")) defp query_list(%{query_params: params}), do: Enum.map(params, ¶meter_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: JsonSchema.schema_for({name, 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 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