Packages
tesla
1.18.2
1.20.0
1.18.3
1.18.2
1.18.1
1.18.0
1.17.0
1.16.0
1.15.3
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.1
1.14.0
1.13.2
1.13.1
1.13.0
1.12.3
1.12.2
1.12.1
1.12.0
1.11.2
1.11.1
1.11.0
1.10.3
1.10.2
1.10.1
1.10.0
1.9.0
1.8.1
retired
1.8.0
1.7.0
1.6.1
1.6.0
1.5.1
1.5.0
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.0
1.0.0
1.0.0-beta.1
0.10.0
0.9.0
0.8.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.2
0.5.1
0.5.0
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.2.2
0.2.1
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
HTTP client library, with support for middleware and multiple adapters.
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/tesla/open_api/path_template.ex
defmodule Tesla.OpenAPI.PathTemplate do
@moduledoc """
A precompiled OpenAPI Path Templating path.
`Tesla.OpenAPI.PathTemplate` keeps the request path as a string while carrying a
compiled representation that `Tesla.Middleware.PathParams` can use to avoid
parsing the same path template on every request.
alias Tesla.OpenAPI.PathTemplate
template = PathTemplate.new!("/items/{id}")
path_params = Tesla.OpenAPI.PathParams.new!([Tesla.OpenAPI.PathParam.new!("id")])
private =
%{}
|> PathTemplate.put_private(template)
|> Tesla.OpenAPI.PathParams.put_private(path_params)
Tesla.get(client, template.path,
opts: [path_params: %{"id" => id}],
private: private
)
Path templates follow the [OpenAPI Path Templating][oas-path-templating]
syntax for `{name}` template expressions.
[oas-path-templating]: https://spec.openapis.org/oas/latest.html#path-templating
"""
@derive {Inspect, except: [:parts]}
@enforce_keys [:path, :parts]
defstruct [:path, :parts]
@typep expression_part ::
{:expr, name :: String.t(), expression :: String.t()}
@typep part :: String.t() | expression_part()
@typep renderer :: (String.t(), String.t(), term() -> iodata())
@opaque t :: %__MODULE__{
path: String.t(),
parts: [part()]
}
@private_key :tesla_path_template
@spec new!(String.t()) :: t()
def new!(path) when is_binary(path) do
parts =
path
|> compile()
|> validate_unique_names!()
%__MODULE__{path: validate_path!(path), parts: parts}
end
@doc """
Adds the compiled path template to `t:Tesla.Env.private/0`.
template = Tesla.OpenAPI.PathTemplate.new!("/items/{id}")
path_params = Tesla.OpenAPI.PathParams.new!([Tesla.OpenAPI.PathParam.new!("id")])
private =
%{}
|> Tesla.OpenAPI.PathTemplate.put_private(template)
|> Tesla.OpenAPI.PathParams.put_private(path_params)
Tesla.get(client, template.path,
opts: [path_params: %{"id" => id}],
private: private
)
"""
@spec put_private(t()) :: Tesla.Env.private()
def put_private(%__MODULE__{} = template) do
put_private(%{}, template)
end
@spec put_private(Tesla.Env.private(), t()) :: Tesla.Env.private()
def put_private(private, %__MODULE__{} = template) when is_map(private) do
Map.put(private, @private_key, template)
end
@doc false
@spec fetch_private(Tesla.Env.private()) :: {:ok, t()} | :error
def fetch_private(private) when is_map(private) do
case Map.fetch(private, @private_key) do
{:ok, %__MODULE__{} = template} ->
{:ok, template}
{:ok, _value} ->
:error
:error ->
:error
end
end
@doc false
@spec render(t(), String.t(), term(), renderer()) ::
{:ok, String.t()} | {:error, :path_mismatch}
def render(%__MODULE__{} = template, path, params, renderer)
when is_binary(path) and is_function(renderer, 3) do
case template.path == path do
true ->
rendered_path =
template.parts
|> render_parts(params, renderer, [])
|> IO.iodata_to_binary()
{:ok, rendered_path}
false ->
{:error, :path_mismatch}
end
end
defp render_parts([], _params, _renderer, parts) do
:lists.reverse(parts)
end
defp render_parts([{:expr, name, expression} | rest], params, renderer, parts) do
part = renderer.(name, expression, params)
render_parts(rest, params, renderer, [part | parts])
end
defp render_parts([literal | rest], params, renderer, parts) do
render_parts(rest, params, renderer, [literal | parts])
end
defp validate_path!("") do
raise ArgumentError, "expected path template path to start with /"
end
defp validate_path!("/" <> _path = path) do
path
end
defp validate_path!(_path) do
raise ArgumentError, "expected path template path to start with /"
end
defp compile(path) do
path
|> compile_parts(0, [])
|> :lists.reverse()
end
defp compile_parts(path, offset, parts) do
case match_template_delimiter(path, offset) do
:nomatch ->
add_literal!(parts, binary_part(path, offset, byte_size(path) - offset))
{index, ?}} ->
raise ArgumentError,
"unexpected closing } in path template #{inspect(path)} at byte #{index}"
{open_index, ?{} ->
compile_template_expression(path, open_index, parts, offset)
end
end
defp compile_template_expression(path, open_index, parts, offset) do
close_offset = open_index + 1
case match_template_delimiter(path, close_offset) do
:nomatch ->
raise ArgumentError, "unclosed template expression in path template #{inspect(path)}"
{_index, ?{} ->
raise ArgumentError,
"nested template expressions are not valid in path template #{inspect(path)}"
{close_index, ?}} ->
compile_closed_template_expression(path, open_index, close_index, parts, offset)
end
end
defp compile_closed_template_expression(path, open_index, close_index, _parts, _offset)
when close_index == open_index + 1 do
raise ArgumentError, "empty template expression in path template #{inspect(path)}"
end
defp compile_closed_template_expression(path, open_index, close_index, parts, offset) do
name_length = close_index - open_index - 1
name = binary_part(path, open_index + 1, name_length)
expression = binary_part(path, open_index, close_index - open_index + 1)
literal = binary_part(path, offset, open_index - offset)
parts = [{:expr, name, expression} | add_literal!(parts, literal)]
compile_parts(path, close_index + 1, parts)
end
defp match_template_delimiter(path, offset) when offset < byte_size(path) do
size = byte_size(path)
case :binary.match(path, ["{", "}"], scope: {offset, size - offset}) do
{index, 1} -> {index, :binary.at(path, index)}
:nomatch -> :nomatch
end
end
defp match_template_delimiter(_path, _offset) do
:nomatch
end
defp add_literal!(parts, "") do
parts
end
defp add_literal!(parts, literal) do
case :binary.match(literal, ["?", "#"]) do
:nomatch ->
[literal | parts]
_match ->
raise ArgumentError, "expected path template path not to include query or fragment"
end
end
defp validate_unique_names!(parts) do
{_names, duplicates} =
Enum.reduce(parts, {MapSet.new(), MapSet.new()}, &track_name/2)
raise_duplicate_names!(duplicates)
parts
end
defp track_name({:expr, name, _expression}, {names, duplicates}) do
case MapSet.member?(names, name) do
true ->
{names, MapSet.put(duplicates, name)}
false ->
{MapSet.put(names, name), duplicates}
end
end
defp track_name(_literal, acc) do
acc
end
defp raise_duplicate_names!(duplicates) do
names =
duplicates
|> MapSet.to_list()
|> Enum.sort()
case names do
[] ->
:ok
names ->
raise ArgumentError,
"duplicate template expressions #{inspect(names)} in path template"
end
end
end