-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Casted conn.params and conn.body_params do not match type spec. Dialyzer raises warnings. #92
Comments
👍 It would be good to provide a custom Phoenix action plug that users can optionally mix in to controllers to get the cast params passed to the handler functions. |
@mbuhot this is what I am using in my project: def action(conn, _) do
name = action_name(conn)
if function_exported?(__MODULE__, name, 3) do
apply(__MODULE__, name, [conn, conn.params, conn.body_params])
else
apply(__MODULE__, name, [conn, conn.params])
end
end It could be changed to use |
It would need to be done in Operation2 |
@moxley Shall we put a v4.0 milestone on this one? I can't come up with a non-breaking way to fix this. |
@mbuhot: Yeah, I think it'll have to be in a major version. |
In case anyone else stumbles on this issue; this is how we solved it for ourselves: defmodule Api.Spex do
alias OpenApiSpex.{Info, OpenApi, Paths}
@behaviour OpenApi
@impl OpenApi
def spec do
%OpenApi{
info: %Info{
title: "My App",
version: "1.0"
},
paths: Paths.from_router(Api.Router)
}
|> OpenApiSpex.resolve_schema_modules()
end
defmacro __using__(opts \\ []) do
operations_module = Keyword.fetch!(opts, :operations_mod)
quote do
plug OpenApiSpex.Plug.CastAndValidate
defdelegate open_api_operation(action), to: unquote(operations_module)
# OpenApiSpex.Plug.CastAndValidate replaces the body_params with a struct based on the input schema; this breaks
# dialyzer compatibility therefore we undo the change here and move the parsed and structified parameters into
# conn.private.spex_params. We also supply them to the controller action as the 2nd argument which is a lot more
# ergonomic than digging into the conn struct.
# See https://github.com/open-api-spex/open_api_spex/issues/92
plug :restore_body_params
def restore_body_params(%Plug.Conn{body_params: %{__struct__: _} = body_params} = conn, _) do
conn
|> Plug.Conn.put_private(:spex_params, body_params)
|> Map.put(:body_params, Map.from_struct(body_params))
end
def restore_body_params(conn, _), do: conn
def action(conn, _) do
name = action_name(conn)
spex_params = conn.private[:spex_params]
apply(__MODULE__, name, [conn, spex_params])
end
end
end
end This allows us to write controllers like this: defmodule Api.UserController do
use Api, :controller
use Api.Spex, operations_mod: Api.Spex.UserOperations
def create(%Plug.Conn{} = conn, %Api.Spex.UserCreateRequestSchema{username: username}) do
# ...
conn |> render(username: username)
end
end ✨ |
Here is a temporary workaround for this problem: defmodule MyAppWeb.MyController do
use MyAppWeb, :controller
use OpenApiSpex.Controller
plug OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true
# Use body params
def create(conn, _params) do
# Dialyzer misses the fact that body_params is being accessed
body_params = Map.get(conn, :body_params)
# The code is now free to reference compile-time atom keys from body params, without complaint from Dialyzer
%{user: user_params} = body_params
end
# Use other params
def create(conn, params) do
# Dialyzer misses the fact that a param value is being accessed using an atom key
id = Map.get(params, :id)
end
end |
I dont think that last workaround will give the |
I'm not saying that it provides the |
Putting this here for future reference. Here is the full output from dialyxer when running it against the example phoenix_app in open_api_spex:
|
The resolution for this issue could happen in three phases: Phase 1: When params are being casted, put them in the conn.private assigns, as mentioned by @bforchhammer. Allow the controller actions to fetch them from the private assigns using convenience functions, such as Phase 2: (Optional) Add plug logic that replaces the second ( Phase 3: Stop having OpenApiSpex modify For Phase 1, a controller action might access casted params like this: def update(conn, _) do
# Request body params fetched by OpenApiSpex.body_params(conn)
# This function might be imported automatically with `use OpenApiSpex.ControllerSpecs`
%UserParams{
name: name,
email: email,
birthday: %Date{} = birthday
} = OpenApiSpex.body_params(conn)
# Path params, query params, and header params, merged together and fetched by OpenApiSpex.params(conn)
# This function might be imported automatically with `use OpenApiSpex.ControllerSpecs`
%{id: id} = OpenApiSpex.params(conn)
end Phase 1 would resolve the bug described in this issue. Phase 2 is for convenience. Phase 3 is cleanup. |
I don't know how you plan to do this, but for phase 1 and 3, I did it this way |
@moxley Is 1 implemented yet or still pending implementation? 3 is going to be a breaking change needing a major release, but we could start with a deprecation warning hinting at migratiing to 1 so that an upgrade path is available today and then release the breaking change with the major. |
relevant: #398 |
* Update depenedencies: OpenAPISpex + cursor based pagination * Update formatter config * Add internal server error implementation * Test errors * Implement pagination interface * Implement Plugins API module macros * Implement Public API base URI (to be used with path helpers once called from within forwarded router's scope) * Implement OpenAPI specs + schemas * Implement Shared Links context module * Add pagination and error views * Add Shared Link view * Implement Shared Link controller * Expose SharedLink.t() spec * Implement separate router for the Plugins API * Update moduledocs * Always wrap resource objects with `data` * Update moduledoc * Use open-api-spex/open_api_spex#425 due to open-api-spex/open_api_spex#92 * Rely on BASE_URL for swagger-ui server definition * Fixup goals migration * Migrate broken goals before deleting dupes * Remove bypassing test rate limiting for which there's none anyway * Move the context module under `Plausible.` namespace * Bring back conn assignment to PluginsAPICase template * Update test/plausible_web/plugins/api/controllers/shared_links_test.exs Co-authored-by: Uku Taht <Uku.taht@gmail.com> * Update renamed aliases * Seed static token for development purposes * Delegate Plugins API 500s to a familiar shape * Simplify with statement --------- Co-authored-by: Uku Taht <Uku.taht@gmail.com>
…ided by the :replace_params config option This allows us to configure Open API Spex to not overwrite the params with the casted versions which violates the Plug.Conn.t() contract open-api-spex/open_api_spex#92 open-api-spex/open_api_spex#425
The type spec for
params
andbody_params
isparams() :: %{required(binary()) => param()}
(https://hexdocs.pm/plug/1.7.2/Plug.Conn.html#t:params/0), but OpenApiSpex turns them into something like%{__struct__: module(), required(atom()) => term()}
, or something else that doesn't match the spec. This causes dialyzer complaints in the controller actions for some situations.IMO, OpenApiSpex shouldn't change
conn.params
orconn.body_params
, as it breaks the contract with Plug. Instead, Spex can put the casted values in the:private
attribute of the%Conn{}
.The text was updated successfully, but these errors were encountered: