Page MenuHomePhorge

No OneTemporary

Size
8 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/open_api_spex/plug/cache.ex b/lib/open_api_spex/plug/cache.ex
new file mode 100644
index 0000000..2deaeab
--- /dev/null
+++ b/lib/open_api_spex/plug/cache.ex
@@ -0,0 +1,50 @@
+defmodule OpenApiSpex.Plug.Cache do
+ @moduledoc false
+
+ @callback get(module) :: {OpenApiSpex.OpenApi.t(), map} | nil
+ @callback put(module, {OpenApiSpex.OpenApi.t(), map}) :: :ok
+
+ @spec adapter() :: OpenApiSpex.Plug.AppEnvCache | OpenApiSpex.Plug.PersistentTermCache
+ def adapter do
+ if function_exported?(:persistent_term, :info, 0) do
+ OpenApiSpex.Plug.PersistentTermCache
+ else
+ OpenApiSpex.Plug.AppEnvCache
+ end
+ end
+end
+
+defmodule OpenApiSpex.Plug.AppEnvCache do
+ @moduledoc false
+ @behaviour OpenApiSpex.Plug.Cache
+
+ @impl OpenApiSpex.Plug.Cache
+ def get(spec_module) do
+ Application.get_env(:open_api_spex, spec_module)
+ end
+
+ @impl OpenApiSpex.Plug.Cache
+ def put(spec_module, spec) do
+ :ok = Application.put_env(:open_api_spex, spec_module, spec)
+ end
+end
+
+if function_exported?(:persistent_term, :info, 0) do
+ defmodule OpenApiSpex.Plug.PersistentTermCache do
+ @moduledoc false
+ @behaviour OpenApiSpex.Plug.Cache
+
+ @impl OpenApiSpex.Plug.Cache
+ def get(spec_module) do
+ :persistent_term.get(spec_module)
+ rescue
+ ArgumentError ->
+ nil
+ end
+
+ @impl OpenApiSpex.Plug.Cache
+ def put(spec_module, spec) do
+ :ok = :persistent_term.put(spec_module, spec)
+ end
+ end
+end
diff --git a/lib/open_api_spex/plug/cast_and_validate.ex b/lib/open_api_spex/plug/cast_and_validate.ex
index 1043094..bc1375c 100644
--- a/lib/open_api_spex/plug/cast_and_validate.ex
+++ b/lib/open_api_spex/plug/cast_and_validate.ex
@@ -1,103 +1,126 @@
defmodule OpenApiSpex.Plug.CastAndValidate do
@moduledoc """
Module plug that will cast and validate the `Conn.params` and `Conn.body_params` according to the schemas defined for the operation.
The operation_id can be given at compile time as an argument to `init`:
plug OpenApiSpex.Plug.CastAndValidate, operation_id: "MyApp.ShowUser"
For phoenix applications, the operation_id can be obtained at runtime automatically.
defmodule MyAppWeb.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.CastAndValidate
...
end
If you want customize the error response, you can provide the `:render_error` option to register a plug which creates
a custom response in the case of a validation error.
## Example
defmodule MyAppWeb.UserController do
use Phoenix.Controller
plug OpenApiSpex.Plug.CastAndValidate, render_error: MyApp.RenderError
...
end
defmodule MyApp.RenderError do
def init(opts), do: opts
def call(conn, reason) do
msg = Jason.encode!(%{error: reason})
conn
|> Conn.put_resp_content_type("application/json")
|> Conn.send_resp(400, msg)
end
end
"""
@behaviour Plug
alias Plug.Conn
@impl Plug
def init(opts) do
opts
|> Map.new()
|> Map.put_new(:render_error, OpenApiSpex.Plug.JsonRenderError)
end
@impl Plug
def call(conn = %{private: %{open_api_spex: private_data}}, %{
operation_id: operation_id,
render_error: render_error
}) do
spec = private_data.spec
operation = private_data.operation_lookup[operation_id]
content_type =
Conn.get_req_header(conn, "content-type")
|> Enum.at(0, "")
|> String.split(";")
|> Enum.at(0)
private_data = Map.put(private_data, :operation_id, operation_id)
conn = Conn.put_private(conn, :open_api_spex, private_data)
with {:ok, conn} <- OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
conn
else
{:error, reason} ->
opts = render_error.init(reason)
conn
|> render_error.call(opts)
|> Plug.Conn.halt()
end
end
def call(
conn = %{
- private: %{phoenix_controller: controller, phoenix_action: action, open_api_spex: _pd}
+ private: %{
+ phoenix_controller: controller,
+ phoenix_action: action,
+ open_api_spex: private_data
+ }
},
opts
) do
- operation_id = controller.open_api_operation(action).operationId
+ operation =
+ case private_data.operation_lookup[{controller, action}] do
+ nil ->
+ operationId = controller.open_api_operation(action).operationId
+ operation = private_data.operation_lookup[operationId]
- if operation_id do
- call(conn, Map.put(opts, :operation_id, operation_id))
+ operation_lookup =
+ private_data.operation_lookup
+ |> Map.put({controller, action}, operation)
+
+ OpenApiSpex.Plug.Cache.adapter().put(
+ private_data.spec_module,
+ {private_data.spec, operation_lookup}
+ )
+
+ operation
+
+ operation ->
+ operation
+ end
+
+ if operation.operationId do
+ call(conn, Map.put(opts, :operation_id, operation.operationId))
else
raise "operationId was not found in action API spec"
end
end
def call(_conn = %{private: %{open_api_spex: _pd}}, _opts) do
raise ":operation_id was neither provided nor inferred from conn. Consider putting plug OpenApiSpex.Plug.Cast rather into your phoenix controller."
end
def call(_conn, _opts) do
raise ":open_api_spex was not found under :private. Maybe OpenApiSpex.Plug.PutApiSpec was not called before?"
end
end
diff --git a/lib/open_api_spex/plug/put_api_spec.ex b/lib/open_api_spex/plug/put_api_spec.ex
index 3188cf1..94d8246 100644
--- a/lib/open_api_spex/plug/put_api_spec.ex
+++ b/lib/open_api_spex/plug/put_api_spec.ex
@@ -1,90 +1,58 @@
defmodule OpenApiSpex.Plug.PutApiSpec do
@moduledoc """
Module plug that calls a given module to obtain the Api Spec and store it as private in the Conn.
This allows downstream plugs to use the API spec for casting, validating and rendering.
## Example
plug OpenApiSpex.Plug.PutApiSpec, module: MyAppWeb.ApiSpec
"""
@behaviour Plug
- @before_compile __MODULE__.Cache
+ @cache OpenApiSpex.Plug.Cache.adapter()
@impl Plug
def init([module: _spec_module] = opts) do
opts[:module]
end
@impl Plug
def call(conn, spec_module) do
{spec, operation_lookup} =
- fetch_spec(spec_module)
+ case @cache.get(spec_module) do
+ nil ->
+ spec = build_spec(spec_module)
+ @cache.put(spec_module, spec)
+ spec
+ spec ->
+ spec
+ end
private_data =
conn
|> Map.get(:private)
|> Map.get(:open_api_spex, %{})
+ |> Map.put(:spec_module, spec_module)
|> Map.put(:spec, spec)
|> Map.put(:operation_lookup, operation_lookup)
Plug.Conn.put_private(conn, :open_api_spex, private_data)
end
@spec build_spec(module) :: {OpenApiSpex.OpenApi.t, %{String.t => OpenApiSpex.Operation.t}}
defp build_spec(mod) do
spec = mod.spec()
operation_lookup = build_operation_lookup(spec)
{spec, operation_lookup}
end
@spec build_operation_lookup(OpenApiSpex.OpenApi.t) :: %{String.t => OpenApiSpex.Operation.t}
defp build_operation_lookup(spec = %OpenApiSpex.OpenApi{}) do
spec
|> Map.get(:paths)
|> Stream.flat_map(fn {_name, item} -> Map.values(item) end)
|> Stream.filter(fn x -> match?(%OpenApiSpex.Operation{}, x) end)
|> Stream.map(fn operation -> {operation.operationId, operation} end)
|> Enum.into(%{})
end
-
- defmodule Cache do
- defmacro __before_compile__(_env) do
- if function_exported?(:persistent_term, :info, 0) do
- quote do
- def fetch_spec(spec_module) do
- try do
- :persistent_term.get(spec_module)
- rescue
- ArgumentError ->
- term_not_found()
- spec = build_spec(spec_module)
- :ok = :persistent_term.put(spec_module, spec)
- spec
- end
- end
-
- defp term_not_found do
- if Application.get_env(:open_api_spex, :persistent_term_warn, false) do
- IO.warn("Warning: the OpenApiSpec spec was deleted from persistent terms. This can cause serious issues.")
- else
- Application.put_env(:open_api_spex, :persistent_term, true)
- end
- end
- end
- else
- quote do
- def fetch_spec(spec_module) do
- case Application.get_env(:open_api_spex, spec_module) do
- nil ->
- spec = build_spec(spec_module)
- Application.put_env(:open_api_spex, spec_module, spec)
- spec
- spec -> spec
- end
- end
- end
- end
- end
- end
end

File Metadata

Mime Type
text/x-diff
Expires
Fri, Nov 29, 2:45 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41231
Default Alt Text
(8 KB)

Event Timeline