# Working with OpenAPI parameters

Tesla does not parse OpenAPI documents or generate client modules. It provides
the request values and middleware needed for generated or hand-written clients
to represent OpenAPI parameter serialization.

## Start with the OpenAPI spec

This example operation has path, query, header, and cookie parameters:

```yaml
paths:
  /items/{id}{coords}:
    get:
      operationId: getItem
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: coords
          in: path
          required: true
          style: matrix
          explode: true
          schema:
            type: array
            items:
              type: string
        - name: color
          in: query
          required: true
          style: pipeDelimited
          schema:
            type: array
            items:
              type: string
        - name: filter
          in: query
          required: true
          style: deepObject
          schema:
            type: object
        - name: X-Request-ID
          in: header
          required: true
          schema:
            type: string
        - name: session_id
          in: cookie
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Item response
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/GetItemResponse"
```

## Build the path module

Start with the operation-owned module for `in: "path"` values and metadata.
The final operation module will store the result of `Path.path_params()` in a
module attribute and pass the request value map to `Tesla.Middleware.PathParams`
in `:modern` mode:

```elixir
defmodule MyApi.Operation.GetItem.Path do
  alias Tesla.OpenAPI.{PathParam, PathParams, PathTemplate}

  @type t :: %__MODULE__{
          id: integer(),
          coords: [String.t()]
        }

  defstruct [:id, :coords]

  @path_template PathTemplate.new!("/items/{id}{coords}")

  @path_params PathParams.new!([
                 PathParam.new!("id"),
                 PathParam.new!("coords", style: :matrix, explode: true)
               ])

  def path_template, do: @path_template
  def path_params, do: @path_params

  def to_path_params(%__MODULE__{} = path) do
    %{
      "id" => path.id,
      "coords" => path.coords
    }
  end
end
```

The runtime struct contains request values only. `path_params/0` returns the
static OpenAPI path metadata that the operation module stores in a module
attribute.

## Build the query module

Start with the operation-owned module for `in: "query"` values and metadata.
The final operation module will store the result of `Query.query_params()` in a
module attribute and pass the request value map to `Tesla.Middleware.Query` in
`:modern` mode:

```elixir
defmodule MyApi.Operation.GetItem.Query do
  alias Tesla.OpenAPI.{QueryParam, QueryParams}

  @type t :: %__MODULE__{
          :"$additional" => map() | nil,
          color: [String.t()],
          filter: keyword()
        }

  defstruct color: nil, filter: nil, "$additional": %{}

  @query_params QueryParams.new!([
                  QueryParam.new!("color", style: :pipe_delimited),
                  QueryParam.new!("filter", style: :deep_object)
                ])

  def query_params, do: @query_params

  def to_query(nil), do: %{}

  def to_query(%__MODULE__{} = query) do
    additional = query."$additional" || %{}

    Map.merge(additional, %{
      "color" => query.color,
      "filter" => query.filter
    })
  end
end
```

`Tesla.OpenAPI.QueryParam` supports the OpenAPI query styles `:form`,
`:space_delimited`, `:pipe_delimited`, and `:deep_object`. Omit optional query
parameters from the returned map when they should not be sent. The operation
module uses the result of `Query.query_params()` when it builds its private
metadata.

Other top-level query params can share the same request query map and remain
normal Tesla query params. This example keeps those values in a generated
`:"$additional"` field.

## Build the header module

For `in: "header"`, expose `Tesla.OpenAPI.HeaderParam` metadata and convert the
request struct into the value map used by `Tesla.OpenAPI.HeaderParams`:

```elixir
defmodule MyApi.Operation.GetItem.Header do
  alias Tesla.OpenAPI.{HeaderParam, HeaderParams}

  @type t :: %__MODULE__{
          request_id: String.t()
        }

  defstruct [:request_id]

  @header_params HeaderParams.new!([
                   HeaderParam.new!("X-Request-ID")
                 ])

  def header_params, do: @header_params

  def to_header_params(nil), do: %{}

  def to_header_params(%__MODULE__{} = headers) do
    %{
      "X-Request-ID" => headers.request_id
    }
  end
end
```

`Tesla.OpenAPI.HeaderParam` supports the OpenAPI header style `:simple`.

## Build the cookie module

For `in: "cookie"`, expose `Tesla.OpenAPI.CookieParam` metadata and convert the
request struct into the value map used by `Tesla.OpenAPI.CookieParams`:

```elixir
defmodule MyApi.Operation.GetItem.Cookie do
  alias Tesla.OpenAPI.{CookieParam, CookieParams}

  @type t :: %__MODULE__{
          session_id: String.t()
        }

  defstruct [:session_id]

  @cookie_params CookieParams.new!([
                   CookieParam.new!("session_id")
                 ])

  def cookie_params, do: @cookie_params

  def to_cookie_params(nil), do: %{}

  def to_cookie_params(%__MODULE__{} = cookies) do
    %{
      "session_id" => cookies.session_id
    }
  end
end
```

`Tesla.OpenAPI.CookieParam` supports the OpenAPI cookie styles `:form` and `:cookie`.

## Build the response wrapper

Generated clients can wrap `Tesla.Env` with the status, headers, and typed
body returned by each operation:

```elixir
defmodule MyApi.Response do
  use Tesla.OpenAPI.Response
end
```

## Build the operation module

Now assemble the nested modules into the generated operation. The nested
modules expose each parameter collection, and the operation module uses those
results when it builds static request metadata:

```elixir
defmodule MyApi.Operation.GetItem do
  alias MyApi.Client
  alias MyApi.Operation.GetItem.{Cookie, Header, Path, Query}
  alias MyApi.Response
  alias Tesla.OpenAPI
  alias Tesla.OpenAPI.{CookieParams, HeaderParams, PathParams, PathTemplate, QueryParams}

  defstruct path: nil,
            query: nil,
            headers: nil,
            cookies: nil

  @type t :: %__MODULE__{
          path: Path.t() | nil,
          query: Query.t() | nil,
          headers: Header.t() | nil,
          cookies: Cookie.t() | nil
        }

  @type resp_body_200() :: MyApi.Schemas.GetItemResponse.t()
  @type resp_header_200() :: Response.headers()
  @type resp_200() :: Response.t(resp_body_200(), resp_header_200())
  @type resp_401() :: Response.t(nil, Response.headers())
  @type resp_404() :: Response.t(nil, Response.headers())
  @type result() :: {:ok, resp_200() | resp_401() | resp_404()} | {:error, term()}

  @operation_path Path.path_template().path
  @header_params Header.header_params()
  @cookie_params Cookie.cookie_params()

  @private OpenAPI.merge_private([
             PathTemplate.put_private(Path.path_template()),
             PathParams.put_private(Path.path_params()),
             QueryParams.put_private(Query.query_params())
           ])

  def new(attrs) when is_map(attrs) do
    %__MODULE__{
      path: Map.fetch!(attrs, :path),
      query: Map.get(attrs, :query),
      headers: Map.get(attrs, :headers),
      cookies: Map.get(attrs, :cookies)
    }
  end

  @doc false
  def handle_operation(%Client{} = client, %__MODULE__{} = operation, opts) do
    headers =
      HeaderParams.to_headers(@header_params, Header.to_header_params(operation.headers)) ++
        CookieParams.to_headers(@cookie_params, Cookie.to_cookie_params(operation.cookies))

    request_opts = [
      method: :get,
      url: @operation_path,
      query: Query.to_query(operation.query),
      headers: headers,
      opts: Keyword.put(opts, :path_params, Path.to_path_params(operation.path)),
      private: @private
    ]

    case Tesla.request(client.client, request_opts) do
      {:ok, %Tesla.Env{status: 200} = env} ->
        {:ok, Response.new(env, MyApi.Schemas.GetItemResponse.new(env.body))}

      {:ok, %Tesla.Env{status: status} = env} when status in [401, 404] ->
        {:ok, Response.new(env, nil)}

      {:ok, env} ->
        {:ok, Response.new(env, env.body)}

      {:error, reason} ->
        {:error, reason}
    end
  end
end
```

## Build the client stack

Use `Tesla.Middleware.PathParams` in `:modern` mode when generated operations
pass `Tesla.OpenAPI.PathParams` through `t:Tesla.Env.private/0`. Use
`Tesla.Middleware.Query` in `:modern` mode when generated operations pass
`Tesla.OpenAPI.QueryParams` through `t:Tesla.Env.private/0`:

```elixir
defmodule MyApi.Client do
  @type t :: %__MODULE__{
          client: Tesla.Client.t()
        }

  defstruct [:client]

  def new(opts) do
    middleware = [
      {Tesla.Middleware.BaseUrl, Keyword.fetch!(opts, :base_url)},
      {Tesla.Middleware.PathParams, mode: :modern},
      {Tesla.Middleware.Query, mode: :modern},
      Tesla.Middleware.JSON
    ]

    adapter = Keyword.fetch!(opts, :adapter)

    %__MODULE__{client: Tesla.client(middleware, adapter)}
  end
end
```

## Build the API module

Expose a generated function that delegates to the operation module:

```elixir
defmodule MyApi do
  alias MyApi.Client
  alias MyApi.Operation.GetItem

  @spec send_get_item(Client.t(), GetItem.t(), keyword()) :: GetItem.result()
  def send_get_item(%Client{} = client, %GetItem{} = operation, opts \\ []) do
    GetItem.handle_operation(client, operation, opts)
  end
end
```

## Send the operation

The caller builds operation values and sends them through the generated
API module:

```elixir
alias MyApi.Operation.GetItem
alias MyApi.Operation.GetItem.{Cookie, Header, Path, Query}

operation =
  GetItem.new(%{
    path: %Path{id: 42, coords: ["blue", "black"]},
    query: %Query{
      :"$additional" => %{"debug" => true},
      color: ["blue", "black"],
      filter: [role: "admin"]
    },
    headers: %Header{request_id: "req-123"},
    cookies: %Cookie{session_id: "abc123"}
  })

MyApi.send_get_item(client, operation, [])
```

## Further reading

- [OpenAPI in Tesla](../explanations/4.openapi.md)
- [OpenAPI Cheat Sheet](../cheatsheets/openapi.cheatmd)
- [OpenAPI Parameter Locations](https://spec.openapis.org/oas/latest.html#parameter-locations)
- [OpenAPI Style Values](https://spec.openapis.org/oas/latest.html#style-values)
- [OpenAPI Style Examples](https://spec.openapis.org/oas/latest.html#style-examples)
