Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F115901
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
8 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
R22 open_api_spex
Attached
Detach File
Event Timeline
Log In to Comment