Current section
Files
Jump to
Current section
Files
lib/oaskit/validation/request_validator.ex
defmodule Oaskit.Validation.RequestValidator do
alias Oaskit.Errors.InvalidBodyError
alias Oaskit.Errors.InvalidParameterError
alias Oaskit.Errors.MissingParameterError
alias Oaskit.Errors.UnsupportedMediaTypeError
alias Oaskit.Plugs.ValidateRequest
alias Oaskit.Validation.RequestData
alias Plug.Conn
@moduledoc """
Base validation logic for requests, used by the `#{inspect(ValidateRequest)}`
Plug.
This module validates a `#{inspect(RequestData)}` struct instead of a
`#{inspect(Plug.Conn)}` struct, which makes it usable from client libraries.
"""
@type private_data :: %{
body_params: map,
query_params: map,
path_params: map,
operation_id: binary
}
@type validation_error ::
InvalidBodyError.t()
| UnsupportedMediaTypeError.t()
| {:parameters_errors, [InvalidParameterError.t() | MissingParameterError.t()]}
| {:not_built, operation_id :: binary}
@doc """
Validates a request and returns cast data (body params, query params and path
params) or an error.
"""
@spec validate_request(RequestData.t(), module, binary) ::
{:ok, private_data} | {:error, validation_error}
def validate_request(%RequestData{} = req_data, spec_module, operation_id) do
case fetch_validations(spec_module, operation_id) do
{:ok, validations_with_root} ->
run_validations(req_data, validations_with_root, operation_id)
{:error, _} = err ->
err
end
end
defp fetch_validations(spec_module, operation_id) do
{validations, jsv_root} = Oaskit.build_spec!(spec_module)
case validations do
%{^operation_id => op_validations} -> {:ok, {op_validations, jsv_root}}
_ -> {:error, {:not_built, operation_id}}
end
end
defp run_validations(req_data, {validations, jsv_root}, operation_id) do
private_accin = %{
body_params: %{},
query_params: %{},
path_params: %{},
operation_id: operation_id
}
Enum.reduce_while(validations, {:ok, private_accin}, fn
validation, {:ok, private} ->
# we are not collecting all errors but rather stop on the first error. If
# parameters are wrong, as they handle path parameters we act as if the
# route is wrong, and do not want to validate the body.
case validate(req_data, validation, jsv_root) do
{:ok, more_private} -> {:cont, {:ok, Map.merge(private, more_private)}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
end
defp validate(req_data, {:parameters, by_location}, jsv_root) do
validate_parameters(req_data, by_location, jsv_root)
end
defp validate(req_data, {:body, required?, media_matchers}, jsv_root) do
validate_body(req_data, req_data.body_params, required?, media_matchers, jsv_root)
end
defp validate_parameters(req_data, by_location, jsv_root) do
%{path_params: raw_path_params, query_params: raw_query_params} = req_data
%{path: path_specs, query: query_specs} = by_location
{cast_path_params, path_errors} =
validate_parameters_group(path_specs, raw_path_params, jsv_root)
{cast_query_params, query_errors} =
validate_parameters_group(query_specs, raw_query_params, jsv_root)
case {path_errors, query_errors} do
{[], []} ->
private = %{path_params: cast_path_params, query_params: cast_query_params}
{:ok, private}
_ ->
{:error, {:parameters_errors, path_errors ++ query_errors}}
end
end
defp validate_parameters_group(param_specs, raw_params, jsv_root) do
Enum.reduce(param_specs, {%{}, []}, fn parameter, {acc, errors} ->
validate_parameter(parameter, raw_params, jsv_root, acc, errors)
end)
end
defp validate_parameter(parameter, raw_params, jsv_root, acc, errors) do
%{bin_key: bin_key, key: key, schema_key: jsv_key, required: required?} = parameter
case Map.fetch(raw_params, bin_key) do
{:ok, value} ->
case validate_with_schema(value, jsv_key, jsv_root) do
{:ok, cast_value} ->
acc = Map.put(acc, key, cast_value)
{acc, errors}
{:error, validation_error} ->
err = %InvalidParameterError{
in: parameter.in,
name: bin_key,
value: value,
validation_error: validation_error
}
{acc, [err | errors]}
end
:error when required? ->
err = %MissingParameterError{in: parameter.in, name: bin_key}
{acc, [err | errors]}
:error ->
{acc, errors}
end
end
defp validate_body(_req_data, body, false = _required?, _, _)
when body in [nil, ""]
when map_size(body) == 0 do
{:ok, %{body_params: nil}}
end
defp validate_body(req_data, body, _required?, media_matchers, jsv_root) do
{primary, secondary} = fetch_content_type(req_data)
with {:ok, {_, jsv_key}} <- match_media_type(media_matchers, {primary, secondary}),
:ok <- ensure_fetched_body!(body),
{:ok, cast_body} <- validate_with_schema(body, jsv_key, jsv_root) do
{:ok, %{body_params: cast_body}}
else
{:error, %JSV.ValidationError{} = validation_error} ->
{:error, %InvalidBodyError{validation_error: validation_error, value: body}}
{:error, :media_type_match} ->
{:error, %UnsupportedMediaTypeError{media_type: "#{primary}/#{secondary}", value: body}}
end
end
defp fetch_content_type(%RequestData{} = req_data) do
%{req_headers: req_headers} = req_data
case List.keyfind(req_headers, "content-type", 0, :error) do
:error ->
{"unknown", "unknown"}
{"content-type", content_type} ->
case Conn.Utils.content_type(content_type) do
:error -> {content_type, ""}
{:ok, primary, secondary, _params} -> {primary, secondary}
end
end
end
@doc false
def match_media_type([{{primary, secondary}, _jsv_key} = matched | _], {primary, secondary}) do
{:ok, matched}
end
def match_media_type([{{"*", _secondary}, _jsv_key} = matched | _], _) do
{:ok, matched}
end
def match_media_type([{{primary, "*"}, _jsv_key} = matched | _], {primary, _}) do
{:ok, matched}
end
def match_media_type([_ | matchspecs], content_type_tuple) do
match_media_type(matchspecs, content_type_tuple)
end
def match_media_type([], _) do
{:error, :media_type_match}
end
defp ensure_fetched_body!(body) do
case body do
%Plug.Conn.Unfetched{} ->
raise ArgumentError,
"body is not fetched, use plug parsers or a custom plug to fetch the body"
_ ->
:ok
end
end
defp validate_with_schema(value, jsv_key, jsv_root)
defp validate_with_schema(value, :no_validation, _) do
{:ok, value}
end
defp validate_with_schema(value, {:precast, caster, jsv_key}, jsv_root) do
case precast_parameter(value, caster) do
{:ok, precast_value} ->
validate_with_schema(precast_value, jsv_key, jsv_root)
{:error, _} ->
# On error we still call the real schema. For instance if "hello" is
# given for the integer type, we will have a correct type error (though
# the value will be quoted if shown in the error message)
validate_with_schema(value, jsv_key, jsv_root)
end
end
defp validate_with_schema(value, jsv_key, jsv_root) do
JSV.validate(value, jsv_root, cast: true, cast_formats: true, key: jsv_key)
end
defp precast_parameter(value, fun) when is_function(fun, 1) do
fun.(value)
end
defp precast_parameter(values, {:list, fun}) when is_list(values) and is_function(fun, 1) do
precast_list(values, fun, [])
end
defp precast_parameter(_values, {:list, fun}) when is_function(fun, 1) do
{:error, :non_array_parameter}
end
defp precast_list([h | t], fun, acc) do
case fun.(h) do
{:ok, v} -> precast_list(t, fun, [v | acc])
{:error, _} = err -> err
end
end
defp precast_list([], _, acc) do
{:ok, :lists.reverse(acc)}
end
end