Skip to content

Commit

Permalink
add: cast parameters with json content-type
Browse files Browse the repository at this point in the history
  • Loading branch information
albertored committed Jun 9, 2022
1 parent 6f80bd4 commit a630e99
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 108 deletions.
24 changes: 23 additions & 1 deletion lib/open_api_spex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ defmodule OpenApiSpex do
opts \\ []
) do
content_type = content_type || content_type_from_header(conn)
Operation2.cast(operation, conn, content_type, spec.components, opts)
Operation2.cast(spec, operation, conn, content_type, opts)
end

defp content_type_from_header(conn = %Plug.Conn{}) do
Expand Down Expand Up @@ -416,4 +416,26 @@ defmodule OpenApiSpex do
"""
@spec params(Plug.Conn.t()) :: nil | map()
def params(%Plug.Conn{} = conn), do: get_in(conn.private, [:open_api_spex, :params])

@doc """
"""
@spec add_parameter_content_parser(
OpenApi.t(),
content_type | [content_type],
parser :: module()
) :: OpenApi.t()
when content_type: String.t() | Regex.t()
def add_parameter_content_parser(%OpenApi{extensions: ext} = spec, content_type, parser) do
extensions = ext || %{}

param_parsers = Map.get(extensions, "x-parameter-content-parsers", %{})

param_parsers =
content_type
|> List.wrap()
|> Enum.reduce(param_parsers, fn ct, acc -> Map.put(acc, ct, parser) end)

%OpenApi{spec | extensions: Map.put(extensions, "x-parameter-content-parsers", param_parsers)}
end
end
94 changes: 71 additions & 23 deletions lib/open_api_spex/cast_parameters.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
defmodule OpenApiSpex.CastParameters do
@moduledoc false
alias OpenApiSpex.{Cast, Components, Operation, Parameter, Reference, Schema}
alias OpenApiSpex.{Cast, OpenApi, Operation, Parameter, Reference, Schema}
alias OpenApiSpex.Cast.Error
alias Plug.Conn

@spec cast(Plug.Conn.t(), Operation.t(), Components.t(), opts :: [OpenApiSpex.cast_opt()]) ::
@default_parsers %{~r/^application\/.*json.*$/ => OpenApi.json_encoder()}

@spec cast(Plug.Conn.t(), Operation.t(), OpenApi.t(), opts :: [OpenApiSpex.cast_opt()]) ::
{:error, [Error.t()]} | {:ok, Conn.t()}
def cast(conn, operation, components, opts \\ []) do
def cast(conn, operation, spec, opts \\ []) do
replace_params = Keyword.get(opts, :replace_params, true)

with {:ok, params} <- cast_to_params(conn, operation, components) do
with {:ok, params} <- cast_to_params(conn, operation, spec) do
{:ok, conn |> cast_conn(params) |> maybe_replace_params(params, replace_params)}
end
end
Expand All @@ -27,11 +29,11 @@ defmodule OpenApiSpex.CastParameters do
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, components) do
defp cast_to_params(conn, operation, %OpenApi{components: components} = spec) do
operation
|> schemas_by_location(components)
|> Enum.map(fn {location, {schema, parameters_contexts}} ->
cast_location(location, schema, parameters_contexts, components, conn)
cast_location(location, schema, parameters_contexts, spec, conn)
end)
|> reduce_cast_results()
end
Expand Down Expand Up @@ -75,7 +77,12 @@ defmodule OpenApiSpex.CastParameters do
# Extract context information from parameters, useful later when casting
defp parameters_contexts(parameters) do
Map.new(parameters, fn parameter ->
{Atom.to_string(parameter.name), Map.take(parameter, [:explode, :style])}
context =
parameter
|> Map.take([:explode, :style])
|> Map.put(:content_type, Parameter.media_type(parameter))

{Atom.to_string(parameter.name), context}
end)
end

Expand Down Expand Up @@ -104,32 +111,73 @@ defmodule OpenApiSpex.CastParameters do
end)
end

defp cast_location(location, schema, parameters_contexts, components, conn) do
params =
get_params_by_location(
conn,
location,
schema.properties |> Map.keys() |> Enum.map(&Atom.to_string/1)
)
|> pre_parse_parameters(parameters_contexts)
defp cast_location(
location,
schema,
parameters_contexts,
%OpenApi{components: components, extensions: ext},
conn
) 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)
end
end

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

Cast.cast(schema, params, components.schemas)
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_parameters(%{} = parameters, %{} = parameters_context) do
Map.new(parameters, fn {key, value} = _parameter ->
{key, pre_parse_parameter(value, Map.get(parameters_context, key, %{}))}
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 Regex.regex?(match) 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) do
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
String.split(parameter, ",")
{:ok, String.split(parameter, ",")}
end

defp pre_parse_parameter(parameter, _context, _parsers) do
{:ok, parameter}
end

defp pre_parse_parameter(parameter, _) do
parameter
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
Expand Down
1 change: 1 addition & 0 deletions lib/open_api_spex/open_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ defmodule OpenApiSpex.OpenApi do
@vendor_extensions ~w(
x-struct
x-validate
x-parameter-content-parsers
)

def json_encoder, do: @json_encoder
Expand Down
11 changes: 6 additions & 5 deletions lib/open_api_spex/operation2.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ defmodule OpenApiSpex.Operation2 do
Cast,
CastParameters,
Components,
OpenApi,
Operation,
Reference,
RequestBody
Expand All @@ -15,23 +16,23 @@ defmodule OpenApiSpex.Operation2 do
alias Plug.Conn

@spec cast(
OpenApi.t(),
Operation.t(),
Conn.t(),
String.t() | nil,
Components.t(),
opts :: [OpenApiSpex.cast_opt()]
) ::
{:error, [Error.t()]} | {:ok, Conn.t()}
def cast(
spec = %OpenApi{components: components},
operation = %Operation{},
conn = %Conn{},
content_type,
components = %Components{},
opts \\ []
) do
replace_params = Keyword.get(opts, :replace_params, true)

with {:ok, conn} <- cast_parameters(conn, operation, components, opts),
with {:ok, conn} <- cast_parameters(conn, operation, spec, opts),
{:ok, body} <-
cast_request_body(operation.requestBody, conn.body_params, content_type, components) do
{:ok, conn |> cast_conn(body) |> maybe_replace_body(body, replace_params)}
Expand All @@ -53,8 +54,8 @@ defmodule OpenApiSpex.Operation2 do
defp maybe_replace_body(conn, _body, false), do: conn
defp maybe_replace_body(conn, body, true), do: %{conn | body_params: body}

defp cast_parameters(conn, operation, components, opts) do
CastParameters.cast(conn, operation, components, opts)
defp cast_parameters(conn, operation, spec, opts) do
CastParameters.cast(conn, operation, spec, opts)
end

defp cast_request_body(ref = %Reference{}, body_params, content_type, components) do
Expand Down
11 changes: 11 additions & 0 deletions lib/open_api_spex/parameter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,15 @@ defmodule OpenApiSpex.Parameter do
{_type, %MediaType{schema: schema}} = Enum.at(content, 0)
schema
end

@doc """
Gets the media type for a parameter, if not present `nil` is returned.
"""
@spec media_type(Parameter.t()) :: String.t() | nil
def media_type(%Parameter{content: content}) when is_map(content) and map_size(content) == 1 do
{type, _} = Enum.at(content, 0)
type
end

def media_type(_), do: nil
end
Loading

0 comments on commit a630e99

Please sign in to comment.