Packages
open_api_spex
3.22.2
3.22.3
3.22.2
3.22.1
3.22.0
3.21.5
3.21.4
3.21.3
3.21.2
3.21.1
3.21.0
3.20.1
3.20.0
3.19.1
3.19.0
3.18.3
3.18.2
3.18.1
3.18.0
3.17.3
3.17.2
3.17.1
3.17.0
3.16.4
3.16.3
3.16.2
3.16.1
3.16.0
3.15.0
3.14.0
3.13.0
3.12.0
3.11.0
3.10.0
3.9.0
3.8.0
3.7.0
3.6.0
3.5.2
3.5.1
3.5.0
3.4.0
3.3.0
3.2.1
3.2.0
3.1.0
3.0.0
2.3.1
2.3.0
2.2.0
2.1.1
2.1.0
2.0.0
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
Leverage Open Api Specification 3 (swagger) to document, test, validate and explore your Plug and Phoenix APIs.
Current section
Files
Jump to
Current section
Files
lib/open_api_spex/cast_parameters.ex
defmodule OpenApiSpex.CastParameters do
@moduledoc false
alias OpenApiSpex.{Cast, OpenApi, Operation, Parameter, Reference, Schema}
alias OpenApiSpex.Cast.Error
alias Plug.Conn
@spec cast(Plug.Conn.t(), Operation.t(), OpenApi.t(), opts :: [OpenApiSpex.cast_opt()]) ::
{:error, [Error.t()]} | {:ok, Conn.t()}
def cast(conn, operation, spec, opts \\ []) do
replace_params = Keyword.get(opts, :replace_params, true)
with {:ok, params} <- cast_to_params(conn, operation, spec, opts) do
{:ok, conn |> cast_conn(params) |> maybe_replace_params(params, replace_params)}
end
end
defp cast_conn(conn, params) do
private_data =
conn
|> Map.get(:private)
|> Map.get(:open_api_spex, %{})
|> Map.put(:params, params)
Plug.Conn.put_private(conn, :open_api_spex, private_data)
end
defp maybe_replace_params(conn, _params, false), do: conn
defp maybe_replace_params(conn, params, true), do: %{conn | params: params}
defp cast_to_params(conn, operation, %OpenApi{components: components} = spec, opts) do
operation
|> schemas_by_location(components)
|> Enum.map(fn {location, {schema, parameters_contexts}} ->
cast_location(location, schema, parameters_contexts, spec, conn, opts)
end)
|> reduce_cast_results()
end
defp get_params_by_location(conn, :query, _) do
Plug.Conn.fetch_query_params(conn).query_params
end
defp get_params_by_location(conn, :path, _) do
conn.path_params
end
defp get_params_by_location(conn, :cookie, _) do
Plug.Conn.fetch_cookies(conn).req_cookies
end
# Matching is done using the downcase version of the expected_names but the original
# version of the expected_names is kept
defp get_params_by_location(conn, :header, expected_names) do
name_lookup = Map.new(expected_names, &{String.downcase(&1), &1})
for {key, value} <- conn.req_headers,
property_name = name_lookup[String.downcase(key)],
into: %{},
do: {property_name, value}
end
defp create_location_schema(parameters, components) do
{
%Schema{
type: :object,
additionalProperties: false,
properties: parameters |> Map.new(fn p -> {p.name, Parameter.schema(p)} end),
required: parameters |> Enum.filter(& &1.required) |> Enum.map(& &1.name)
}
|> maybe_add_additional_properties(components),
parameters_contexts(parameters)
}
end
# Extract context information from parameters, useful later when casting
defp parameters_contexts(parameters) do
Map.new(parameters, fn parameter ->
context =
parameter
|> Map.take([:explode, :style])
|> Map.put(:content_type, Parameter.media_type(parameter))
{Atom.to_string(parameter.name), context}
end)
end
defp schemas_by_location(operation, components) do
param_specs_by_location =
operation.parameters
|> Enum.map(fn
%Reference{} = ref ->
Reference.resolve_parameter(ref, components.parameters)
%Parameter{schema: ref = %Reference{"$ref": "#/components/parameters/" <> _}} = param ->
schema = Reference.resolve_parameter(ref, components.parameters)
%{param | schema: schema}
%Parameter{schema: ref = %Reference{"$ref": "#/components/schemas/" <> _}} = param ->
schema = Reference.resolve_schema(ref, components.schemas)
%{param | schema: schema}
%Parameter{} = param ->
param
end)
|> Enum.group_by(& &1.in)
Map.new(param_specs_by_location, fn {location, parameters} ->
{location, create_location_schema(parameters, components)}
end)
end
defp cast_location(
location,
schema,
parameters_contexts,
%OpenApi{components: components, extensions: ext},
conn,
opts
) do
parsers = Map.get(ext || %{}, "x-parameter-content-parsers", %{})
parsers = Map.merge(default_parsers(), parsers)
conn
|> get_params_by_location(
location,
schema.properties |> Map.keys() |> Enum.map(&Atom.to_string/1)
)
|> pre_parse_parameters(parameters_contexts, parsers)
|> case do
{:error, _} = err -> err
params -> Cast.cast(schema, params, components.schemas, opts)
end
end
defp default_parsers,
do: %{~r/^application\/.*json.*$/ => OpenApi.json_encoder()}
defp pre_parse_parameters(%{} = parameters, %{} = parameters_context, parsers) do
Enum.reduce_while(parameters, Map.new(), fn {key, value}, acc ->
case pre_parse_parameter(value, Map.get(parameters_context, key, %{}), parsers) do
{:ok, param} -> {:cont, Map.put(acc, key, param)}
err -> {:halt, err}
end
end)
end
defp pre_parse_parameter(parameter, %{content_type: content_type}, parsers)
when is_bitstring(content_type) and is_map_key(parsers, content_type) do
parser = Map.fetch!(parsers, content_type)
decode_parameter(parameter, content_type, parser)
end
defp pre_parse_parameter(parameter, %{content_type: content_type}, parsers)
when is_bitstring(content_type) do
Enum.reduce_while(parsers, {:ok, parameter}, fn {match, parser}, acc ->
if is_struct(match, Regex) and Regex.match?(match, content_type) do
{:halt, decode_parameter(parameter, content_type, parser)}
else
{:cont, acc}
end
end)
end
defp pre_parse_parameter(parameter, %{explode: false, style: :form} = _context, _parsers) do
# e.g. sizes=S,L,M
# This does not take care of cases where the value may contain a comma itself
{:ok, String.split(parameter, ",")}
end
defp pre_parse_parameter(parameter, _context, _parsers) do
{:ok, parameter}
end
defp decode_parameter(parameter, content_type, parser) when is_atom(parser) do
decode_parameter(parameter, content_type, &parser.decode/1)
end
defp decode_parameter(parameter, content_type, parser) when is_function(parser, 1) do
case parser.(parameter) do
{:ok, result} -> {:ok, result}
{:error, _error} -> Cast.error(%Cast{}, {:invalid_format, content_type})
end
end
defp reduce_cast_results(results) do
Enum.reduce_while(results, {:ok, %{}}, fn
{:ok, params}, {:ok, all_params} -> {:cont, {:ok, Map.merge(all_params, params)}}
cast_error, _ -> {:halt, cast_error}
end)
end
defp maybe_add_additional_properties(schema, components) do
ap_schema =
schema.properties
|> Enum.map(fn
{name, %Schema{} = schema} ->
{name, schema}
{name, %Reference{"$ref": "#/components/schemas/" <> _} = ref} ->
{name, Reference.resolve_schema(ref, components.schemas)}
end)
|> Enum.filter(fn
{_name, %{additionalProperties: ap}} -> ap
_ -> false
end)
case ap_schema do
[{_, %{additionalProperties: ap}}] -> %{schema | additionalProperties: ap}
_ -> schema
end
end
end