defmodule CircleCI.Operation do @moduledoc """ Defines a struct that tracks client requests > #### Note {:.info} > > This module is unlikely to be used directly by applications. Instead, functions in this module > are useful for plugins. See `CircleCI.Plugin` for more information. ## Fields * `private` (map): This field is useful for plugins that need to store information. Plugins should be careful to namespace their data to avoid overlap. By default, this map will include an `__auth__` key with the auth credentials used for the request, `__info__` containing the information that originated the request, and `__opts__` containing all of the options passed in to the original operation function. * `request_body` (term): For requests that support request bodies, this key will hold the data to be included in an outgoing request. Depending on the plugins involved, this key may have Elixir terms (like a map) or strings (such as a JSON-encoded string). * `request_headers` (list of headers): HTTP headers to be included in the outgoing request. These are specified as tuples with the name of the header and its value. * `request_method` (atom): HTTP verb of the outgoing request. * `request_params` (keyword): URL-based query parameters for the outgoing request. * `request_server` (string): URL scheme and hostname of the API server for the request. * `request_types` (list of types): OpenAPI type specifications for the request body. These are specified as tuples with the Content-Type and the type specification. * `request_url` (string): URL path of the outgoing request. * `response_body` (term): For responses that include response bodies, this key will hold the data from the response. Depending on the plugins involved, this key may have raw response data (such as a JSON-encoded string) or Elixir terms (like a map). * `response_code` (integer): Response status code. * `response_headers` (list of headers): HTTP headers from the response. These are specified as tuples with the name of the header and its value. * `response_types` (list of types): OpenAPI type specifications for the response body. These are specified as tuples with the status code and the type specification. """ alias CircleCI.Auth alias CircleCI.Config @type auth :: nil | (token :: String.t()) | {username :: String.t(), password :: String.t()} @type header :: {String.t(), String.t()} @type headers :: [header] @type method :: :get | :put | :post | :delete | :options | :head | :patch | :trace @type request_type :: {String.t(), type} @type request_types :: [request_type] @type response_type :: {integer, type | nil} @type response_types :: [response_type] @typedoc "Type annotation produced by [OpenAPI](https://github.com/aj-foster/open-api-generator)" @type type :: :binary | :boolean | :integer | :map | :number | :string | :unknown | {:array, type} | {:nullable, type} | {:union, [type]} | {module, atom} @typedoc "Operation struct for tracking client requests from start to finish" @type t :: %__MODULE__{ private: map, request_body: term, request_headers: headers, request_method: method, request_params: keyword | [{String.t(), String.t()}] | nil, request_server: String.t(), request_types: request_types | nil, request_url: String.t(), response_body: term, response_code: integer | nil, response_headers: headers | nil, response_types: response_types } defstruct [ :private, :request_body, :request_headers, :request_method, :request_params, :request_server, :request_types, :request_url, :response_body, :response_code, :response_headers, :response_types ] # # Plugin Helpers # @doc """ Get the client's calling function and original arguments This level of introspection is meant for testing purposes, although other plugins can take advantage of it as necessary. """ @spec get_caller(t) :: {module, atom, [any]} def get_caller(operation) do case operation do %__MODULE__{private: %{__info__: %{args: args, call: {module, function}}}} -> {module, function, Keyword.values(args)} %__MODULE__{private: %{__info__: %{call: {module, function}}}} -> {module, function, []} end end @doc """ Get the options passed to the client request ## Examples iex> operation = %Operation{private: %{__opts__: [server: "https://api.github.com"]}} iex> Operation.get_options(operation) [server: "https://api.github.com"] """ @spec get_options(t) :: keyword def get_options(%__MODULE__{private: %{__opts__: opts}}), do: opts @doc """ Get the value of a response header If response headers have not been filled in — or the response did not have the given header — then `nil` will be returned. ## Examples iex> operation = %Operation{response_headers: [{"Content-Type", "application/json"}]} iex> Operation.get_response_header(operation, "Content-Type") "application/json" iex> operation = %Operation{response_headers: []} iex> Operation.get_response_header(operation, "ETag") nil """ @spec get_response_header(t, String.t()) :: String.t() | nil def get_response_header(operation, header) do %__MODULE__{response_headers: headers} = operation header = String.downcase(header) case Enum.find(headers, fn {name, _value} -> String.downcase(name) == header end) do {_header, value} -> value _ -> nil end end @doc """ Put information in the operation's private data store Existing data with the same key will be overridden. ## Example iex> operation = %Operation{private: %{}} iex> operation = Operation.put_private(operation, :my_plugin_data, "abc123") %Operation{private: %{my_plugin_data: "abc123"}} = operation """ @spec put_private(t, atom, term) :: t def put_private(operation, key, value) do %__MODULE__{private: private} = operation %__MODULE__{operation | private: Map.put(private, key, value)} end @doc """ Add a request header to an outgoing operation This function makes no effort to deduplicate headers. ## Example iex> operation = %Operation{request_headers: []} iex> operation = Operation.put_request_header(operation, "Content-Type", "application/json") %Operation{request_headers: [{"Content-Type", "application/json"}]} = operation """ @spec put_request_header(t, String.t(), String.t()) :: t def put_request_header(operation, header, value) do %__MODULE__{request_headers: headers} = operation %__MODULE__{operation | request_headers: [{header, value} | headers]} end # # Internal # @doc false @spec new(map) :: t def new( %{ url: url, method: method, response: response_types, opts: opts } = request_info ) do %__MODULE__{ private: %{__info__: request_info, __opts__: opts}, request_body: request_info[:body], request_headers: [], request_method: method, request_params: request_info[:query], request_server: Config.server(opts), request_types: request_info[:request], request_url: url, response_types: response_types } |> put_auth_header(opts[:auth]) |> put_content_type_header() |> put_accept_header() |> put_user_agent() end @spec put_auth_header(t, auth) :: t defp put_auth_header(operation, nil) do if auth = Config.default_auth() do put_auth_header(operation, auth) else put_private(operation, :__auth__, nil) end end defp put_auth_header(operation, token) when is_binary(token) do operation |> put_request_header("Authorization", "Bearer #{token}") |> put_private(:__auth__, token) end defp put_auth_header(operation, value) do auth = Auth.to_auth(value) put_auth_header(operation, auth) end @spec put_content_type_header(t) :: t defp put_content_type_header(%__MODULE__{request_types: nil} = operation), do: operation defp put_content_type_header(%__MODULE__{request_types: [type]} = operation) do {content_type, _body_type} = type put_request_header(operation, "Content-Type", content_type) end @spec put_accept_header(t) :: t defp put_accept_header(operation) do put_request_header(operation, "Accept", "application/json") end @spec put_user_agent(t) :: t defp put_user_agent(operation) do user_agent = IO.iodata_to_binary([ Config.app_name() || "Unknown App", " via circleci_client ", Application.spec(:circleci_client, :vsn), "; Elixir ", System.version(), " / OTP ", System.otp_release() ]) put_request_header(operation, "User-Agent", user_agent) end end