Cannot access struct properties

Hi guys, I have this problem when I cannot access a struct property when it’s passed as a parameter in a function, for e.g.:

defp user_type(%User{} = current_user, %User{} = target_user)
when current_user.id == target_user.id,
           do: :ok

or

  defp user_type(current_user, target_user)
       when current_user.id == target_user.id,
       do: :ok

Getting same exact error:

== Compilation error in file lib/user_restfulapi_phx_web/controllers/user_controller.ex ==
** (CompileError) lib/user_restfulapi_phx_web/controllers/user_controller.ex:55: cannot invoke remote function current_user.id/0 inside guard
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    lib/user_restfulapi_phx_web/controllers/user_controller.ex:54: (module)
    (stdlib) erl_eval.erl:677: :erl_eval.do_apply/6

Cheers

Guards are designed to be very limited (so that they can be very efficient). This means that you cannot make any function calls which is why you get the error cannot invoke remote function current_user.id/0. It may not look like it but current_user.id is a function call (you can also write it like current_user.id().

To accomplish your original goal you could write your code like:

defp user_type(%User{id: current_user_id}, %User{id: target_user_id}), do: :ok

Since you can make use of previous values matched you can even write it like:

defp user_type(%User{id: current_user_id}, %User{id: current_user_id}), do: :ok

But don’t forget the negative cases as well. Not sure if you want to return false or :error here

defp user_type(_, _), do: false
1 Like

Thanks for the reply, however, in other goards im using other struct propes, so im not sure how to apply this method to it:

  def update(conn, %{"id" => id, "user" => user_params}) do
    current_user = Guardian.Plug.current_resource(conn)
    user = Accounts.get_user!(id)

    with :ok <- user_type(current_user, user),
         {:ok, %User{} = user} <- Accounts.update_user(user, user_params) do
      render(conn, "show.json", user: user)
    end
  end

  defp user_type(%User{} = current_user, %User{} = target_user)
       when current_user.id == target_user.id,
       do: :ok

  defp user_type(%User{} = current_user, %User{} = target_user) when current_user.role == "admin",
    do: :ok

  defp user_type(%User{} = current_user, %User{} = target_user), do: {:error, :unauthorized}

EDIT: Made it work using the solution below:

  def update(conn, %{"id" => id, "user" => user_params}) do
    current_user = Guardian.Plug.current_resource(conn)
    user = Accounts.get_user!(id)

    with :ok <- user_type(current_user, user),
         {:ok, %User{} = user} <- Accounts.update_user(user, user_params) do
      render(conn, "show.json", user: user)
    end
  end

  defp user_type(%User{id: cid} = current_user, %User{id: tid} = target_user)
       when cid == tid,
       do: :ok

  defp user_type(%User{role: role} = current_user, _target_user) when role == "admin",
    do: :ok

  defp user_type(_current_user, _target_user), do: {:error, :unauthorized}

Is there any way this code be improved?

Is there any way this code be improved?

Maybe remove unnecessary guards and add specs …

@spec user_type(current_user :: %User{}, target_user :: %User{}) :: :ok | {:error, :unauthorized}
defp user_type(%User{id: id}, %User{id: id}), do: :ok
defp user_type(%User{role: "admin"}, _target_user), do: :ok
defp user_type(_current_user, _target_user), do: {:error, :unauthorized}
2 Likes