Well that’s because handle_in
receives a map with string keys.
voughtdq came up with Poison.decode … as a nice solution (map to struct).
Using Poison.decode eliminates the need for boilerplate code in the struct definition, yay!
So that’s the only reason why I would want to use it.
I tend to use Ecto and embedded schemas for map -> struct and boundary validation like this:
defmodule MyApp.MyContext do
def do_the_thing(params \\ %{}) when is_map(params) do
with {:ok, command} <- MyApp.MyContext.Commands.DoTheThing.new(params) do
# fun business logic here
end
end
end
defmodule MyApp.MyContext.Commands.DoTheThing do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field :field_1, :string
field :field_2, :integer
end
def new(params) do
command = changeset(params)
case command.valid? do
true -> {:ok, apply_changes(command)}
false -> {:error, command.errors}
end
end
defp changeset(params) do
%__MODULE__{}
|> cast(params, [:field_1, :field_2])
|> validate_required([:field_1, :field_2])
end
end
It doesn’t feel so dirty to do this since Ecto 3.0 and the :ecto_sql
separation.
I’ve found this to be a good way to accept large form inputs - maybe not as practical for smaller sets of parameters.
Tnx, I understand your point, it’s probably better to use something solid like ecto.
Your new
function is similar to initializing a struct in Go, is this typical also for Elixir?
Most of my channel functions accept just a few parameters and is_binary
is fine most of the time.
I just don’t like defining the map as an argument and all of the guards; it doesn’t look nice.
still a little bit dirty
@primary_key false
embedded_schema do ...
@MrDoops suppose I would really like to embed my structures
in my channel modules,
and I wouldn’t want to define new
and changeset
functions for all of my embeds.
Would it be crazy to do something like this:
defmodule MyApp.SomeChannel do
use Phoenix.Channel
defmodule SigninData do
use Ecto.Schema
use MyApp.Macros.Structure
@primary_key false
embedded_schema do
field :email, :string
field :password, :string
end
end
...
def handle_in("signin", %{} = params, socket) do
params
|> SigninData.new
|> ... authenticate ...
|> ... reply ...
end
end
using this macro:
defmodule MyApp.Macros.Structure do
import Ecto.Changeset
defmacro __using__(_opts) do
quote do
def new(params) do
params |> MyApp.Macros.Structure.new(unquote(__CALLER__.module))
end
end
end
def new(params, module) when is_map(params), do: params |> changeset(module) |> structure
def new(_params, _module), do: {:error, :invalid}
defp changeset(params, module) do
with fields <- module.__schema__(:fields) do
struct(module)
|> cast(params, fields)
|> validate_required(fields)
end
end
defp structure(%Ecto.Changeset{valid?: true} = changeset), do: {:ok, apply_changes(changeset)}
defp structure(%Ecto.Changeset{} = changeset), do: {:error, changeset.errors}
defp structure(_noop), do: {:error, :unprocessable_entity}
end
?
It’s mostly a subjective thing, but I prefer not to have the Phoenix / Web layer know how Identity / Authentication is implemented. I’d rather do something like:
def handle_in("signin", %{} = params, socket) do
case Identity.authenticate(params) do
{:ok, ...} -> ...
{:error, ...} -> ...
end
end
Fair point, I would actually also delegate the actual authentication process.
I was trying to better define the channel api.
I’m looking into Absynthe / GraphQL to better define my api.
It seems to make the structs inside channels thing irrelevant and provides a ton of functionality like user input validation.
Not sure if this is exactly your problem, but since I was searching for "How to convert a map to a User struct, and this topic popped up, I thought that I would add what I eventually did that worked perfectly:
Map.merge(%User{}, map_defaults)
This is dangerous as it doesn’t check that the keys in map_defaults
actually correspond to keys of the struct. You may end up with a broken struct which is annoying to debug. What you should do is:
s = struct!(User, map_defaults)
This will ensure that keys that are in @enforce_keys
must be exist and there are no keys that don’t exist in the struct. Otherwise it will raise an error. There is also Kernel.struct/2
(i.e. without exlamation mark) that will just discard unknown keys and won’t enforce @enforce_keys
.
Much better yes. I didn’t know this one. I wonder why the web searches didn’t pull this up, if it was that easy to do. First I’ve heard of ‘struct/2’ thanks!
I encountered a similar problem today. Wanted a minimal set of fields to process frequently so created a smaller struct, with only a subset of key/value pairs of the full one. Then I fetch data from the DB in a manner very similar to the one described here:
IOW I am getting a Map
(well, list of maps in that particular example there) with only the key/value pairs I am interested in and pass it to struct!2
struct!(%MyMinimalStruct{}, my_map_from_db_query)
Seems almost suspiciously trivial… any gotchas?
Alternative could be to create another “model” with limited Ecto.Schema
defnition. Might check that route too
You can do from x in query, select: %MyMinimalStruct{id: x.id, name: x.name}
. A bit more verbose than the map/2
syntax, but no later mapping needed. If MyMinimalStruct
is also a schema module there’s also Repo.load
.
Excellent! Verbosity difference with minimal fieldset is a non-issue. Something like this:
MyModel
|> select([mm], %MyMinimalStruct{id: mm.id, name: mm.name}
|> Repo.get_by!(id: id)
LGTM. And can be used instead of converting maps in cases where only the (minimal) struct is needed.