defmodule OpenAPI.Processor.Operation do @moduledoc """ Default plugin for formatting operations This module also provides the Operation struct that is used by the renderer. """ alias OpenAPI.Processor.Operation.Param alias OpenAPI.Processor.State alias OpenAPI.Processor.Type alias OpenAPI.Spec.Path.Operation, as: OperationSpec alias OpenAPI.Spec.RequestBody, as: RequestBodySpec alias OpenAPI.Spec.Response, as: ResponseSpec alias OpenAPI.Spec.Schema, as: SchemaSpec alias OpenAPI.Spec.Schema.Media, as: MediaSpec @typedoc "HTTP method" @type method :: :get | :put | :post | :delete | :options | :head | :patch | :trace @typedoc "Request content types and their associated schema specs" @type request_body_unprocessed :: [{content_type :: String.t(), schema :: SchemaSpec.t()}] @typedoc "Request content types and their associated schemas" @type request_body :: [{content_type :: String.t(), schema :: Type.t()}] @typedoc "Response status codes and their associated schema specs" @type response_body_unprocessed :: [ {status :: integer | :default, schemas :: %{String.t() => SchemaSpec.t()}} ] @typedoc "Response status codes and their associated schemas" @type response_body :: [{status :: integer | :default, schemas :: %{String.t() => Type.t()}}] @typedoc "Processed operation data used by the renderer" @type t :: %__MODULE__{ docstring: String.t(), function_name: atom, module_name: atom, request_body: request_body, request_method: atom, request_path: String.t(), request_path_parameters: [Param.t()], request_query_parameters: [Param.t()], responses: response_body } defstruct [ :docstring, :function_name, :module_name, :request_body, :request_method, :request_path, :request_path_parameters, :request_query_parameters, :responses ] @doc """ Create the contents of an `@doc` string for the given operation Default implementation of `c:OpenAPI.Processor.operation_docstring/3`. The docstring constructed by this function will contain a summary line provided by the operation summary (if available) or the request method and path otherwise. It will incorporate the operation description (if available) and link to any included external documentation. If the operation has query parameters, they are documented in an "Options" section as they are part of the `opts` argument. If the operation has a request body, it's documented in a "Request Body" section with content types and description. @doc \"\"\" Summary of the operation or method and path Description of the operation, which generally provides more information. ## Options * `param`: query parameter description ## Request Body **Content Types**: `application/json` Description of the request body ## Resources * [External Doc Description](link to external documentation) \"\"\" """ @doc default_implementation: true @spec docstring(State.t(), OperationSpec.t(), [Param.t()]) :: String.t() def docstring(_state, operation, query_params) do %OperationSpec{ "$oag_path": request_path, "$oag_path_method": request_method, description: description, external_docs: external_docs, request_body: request_body, summary: summary } = operation summary = if summary do "#{summary}\n" else "#{request_method} `#{request_path}`\n" end description = if description not in [nil, ""], do: "\n#{description}\n" options = if length(query_params) > 0 do for %Param{description: description, name: name} <- query_params, into: "\n## Options\n\n" do if description do description = String.replace(description, "\n", "\n ") " * `#{name}`: #{description}\n" else " * `#{name}`\n" end end <> "\n" end body_params = if request_body do %RequestBodySpec{ description: description, content: content } = request_body content_types = content |> Map.keys() |> Enum.map(&"`#{&1}`") |> Enum.join(", ") if description do "\n## Request Body\n\n**Content Types**: #{content_types}\n\n#{description}\n" else "\n## Request Body\n\n**Content Types**: #{content_types}\n" end end resources = if external_docs && external_docs.url do if external_docs.description do """ ## Resources * [#{external_docs.description}](#{external_docs.url}) """ else """ ## Resources * [Documentation](#{external_docs.url}) """ end end String.replace( "#{summary}#{description}#{options}#{body_params}#{resources}", "\n\n\n", "\n\n" ) end @doc """ Collect request content types and their associated schemas Default implementation of `c:OpenAPI.Processor.operation_request_body/2`. """ @doc default_implementation: true @spec request_body(State.t(), OperationSpec.t()) :: request_body_unprocessed def request_body(_state, %OperationSpec{request_body: %RequestBodySpec{content: content}}) when is_map(content) do Enum.map(content, fn {content_type, %MediaSpec{schema: schema}} -> {content_type, schema} end) end def request_body(_state, _operation_spec), do: [] @doc """ Cast the HTTP method to an atom Default implementation of `c:OpenAPI.Processor.operation_request_method/2`. """ @doc default_implementation: true @spec request_method(State.t(), OperationSpec.t()) :: method def request_method(_state, %OperationSpec{"$oag_path_method": "get"}), do: :get def request_method(_state, %OperationSpec{"$oag_path_method": "put"}), do: :put def request_method(_state, %OperationSpec{"$oag_path_method": "post"}), do: :post def request_method(_state, %OperationSpec{"$oag_path_method": "delete"}), do: :delete def request_method(_state, %OperationSpec{"$oag_path_method": "options"}), do: :options def request_method(_state, %OperationSpec{"$oag_path_method": "head"}), do: :head def request_method(_state, %OperationSpec{"$oag_path_method": "patch"}), do: :patch def request_method(_state, %OperationSpec{"$oag_path_method": "trace"}), do: :trace @doc """ Collect response status codes and their associated schemas Default implementation of `c:OpenAPI.Processor.operation_response_body/2`. In this implementation, all schemas are returned regardless of content type. It is possible for the same status code to have multiple schemas, in which case the renderer should compose a union type for the response. """ @doc default_implementation: true @spec response_body(State.t(), OperationSpec.t()) :: response_body_unprocessed def response_body(_state, %OperationSpec{responses: responses}) when is_map(responses) do Enum.map(responses, fn {status_or_default, %ResponseSpec{content: content}} -> schemas = Map.new(content, fn {content_type, %MediaSpec{schema: schema}} -> {content_type, schema} end) {status_or_default, schemas} end) end end