Merge branch 'feat/openapi-spec-export' into 'develop'
[akkoma] / lib / pleroma / web / api_spec / cast_and_validate.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
3 # Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
4 # SPDX-License-Identifier: AGPL-3.0-only
5
6 defmodule Pleroma.Web.ApiSpec.CastAndValidate do
7 @moduledoc """
8 This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
9 (https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
10 The main difference is ignoring unexpected query params instead of throwing
11 an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
12 to disable this behavior. Also, the default rendering error module
13 is `Pleroma.Web.ApiSpec.RenderError`.
14 """
15
16 @behaviour Plug
17
18 alias Plug.Conn
19
20 @impl Plug
21 def init(opts) do
22 opts
23 |> Map.new()
24 |> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
25 end
26
27 @impl Plug
28 def call(%{private: %{open_api_spex: private_data}} = conn, %{
29 operation_id: operation_id,
30 render_error: render_error
31 }) do
32 spec = private_data.spec
33 operation = private_data.operation_lookup[operation_id]
34
35 content_type =
36 case Conn.get_req_header(conn, "content-type") do
37 [header_value | _] ->
38 header_value
39 |> String.split(";")
40 |> List.first()
41
42 _ ->
43 "application/json"
44 end
45
46 private_data = Map.put(private_data, :operation_id, operation_id)
47 conn = Conn.put_private(conn, :open_api_spex, private_data)
48
49 case cast_and_validate(spec, operation, conn, content_type, strict?()) do
50 {:ok, conn} ->
51 conn
52
53 {:error, reason} ->
54 opts = render_error.init(reason)
55
56 conn
57 |> render_error.call(opts)
58 |> Plug.Conn.halt()
59 end
60 end
61
62 def call(
63 %{
64 private: %{
65 phoenix_controller: controller,
66 phoenix_action: action,
67 open_api_spex: private_data
68 }
69 } = conn,
70 opts
71 ) do
72 operation =
73 case private_data.operation_lookup[{controller, action}] do
74 nil ->
75 operation_id = controller.open_api_operation(action).operationId
76 operation = private_data.operation_lookup[operation_id]
77
78 operation_lookup =
79 private_data.operation_lookup
80 |> Map.put({controller, action}, operation)
81
82 OpenApiSpex.Plug.Cache.adapter().put(
83 private_data.spec_module,
84 {private_data.spec, operation_lookup}
85 )
86
87 operation
88
89 operation ->
90 operation
91 end
92
93 if operation.operationId do
94 call(conn, Map.put(opts, :operation_id, operation.operationId))
95 else
96 raise "operationId was not found in action API spec"
97 end
98 end
99
100 def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
101
102 defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
103 OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
104 end
105
106 defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
107 case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
108 {:ok, conn} ->
109 {:ok, conn}
110
111 # Remove unexpected query params and cast/validate again
112 {:error, errors} ->
113 query_params =
114 Enum.reduce(errors, conn.query_params, fn
115 %{reason: :unexpected_field, name: name, path: [name]}, params ->
116 Map.delete(params, name)
117
118 # Filter out empty params
119 %{reason: :invalid_type, path: [name_atom], value: ""}, params ->
120 Map.delete(params, to_string(name_atom))
121
122 %{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
123 path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
124 update_in(params, path, &List.delete(&1, value))
125
126 _, params ->
127 params
128 end)
129
130 conn = %Conn{conn | query_params: query_params}
131 OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
132 end
133 end
134
135 defp list_items_to_string(list) do
136 Enum.map(list, fn
137 i when is_atom(i) -> to_string(i)
138 i -> i
139 end)
140 end
141
142 defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
143 end