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:
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:
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
endThe 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:
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
endTesla.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:
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
endTesla.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:
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
endTesla.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:
defmodule MyApi.Response do
use Tesla.OpenAPI.Response
endBuild 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:
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
endBuild the client stack
Use Tesla.Middleware.PathParams in :modern mode when generated operations
pass Tesla.OpenAPI.PathParams through Tesla.Env.private/0. Use
Tesla.Middleware.Query in :modern mode when generated operations pass
Tesla.OpenAPI.QueryParams through Tesla.Env.private/0:
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
endBuild the API module
Expose a generated function that delegates to the operation module:
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
endSend the operation
The caller builds operation values and sends them through the generated API module:
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, [])