The decode as
stuff will not compile in conjunction with @enforce_keys
.
Probably this is related to the absence of the %Stuff{} = my_stuff
feature.
At the risk of stating the obvious
defmodule Stuff do
defstruct foo: nil, bar: nil
def from_map(%{"foo" => foo, "bar" => bar}),
do: %Stuff{foo: foo, bar: bar}
# alternatively - return some error
def from_map(m),
do: m
#...
end
# ...
def handle_in("send_stuff", stuff, socket) do
with %Stuff{} = my_stuff <- Stuff.from_map(stuff) do
something_with(my_stuff)
else
# do something else with "stuff" argument ...
end
end
# ... or
def handle_in("send_stuff", stuff, socket) do
case Stuff.from_map(stuff) do
%Stuff{} = my_stuff ->
something_with(my_stuff)
# other options ...
_ ->
# ...
end
end
Tnx for your suggestions, much appreciated.
I’m trying to avoid boilerplate code in struct definitions and
if/else/with/case statements in functions (I prefer pattern matching).
I am thinking about something like this, using a helper function:
def to_struct(%{} = params, kind) do
params
|> Poison.encode
|> (fn {:ok, json} -> json end).()
|> Poison.decode(%{as: kind, keys: :atoms!})
|> (fn {:ok, decoded} -> decoded end).()
end
def handle_in("send_stuff", %{} = m, socket) do
m |> to_struct(%Stuff{}) |> something_with
end
defp something_with(%Stuff{} = stuff), do: ...
Or maybe I should just skip decoding/encoding and do something like this:
// javascript
let data = { "json": JSON.stringify(actual_stuff) };
// push...
// elixir
def handle_in("send_stuff", %{"json" => json}, socket) do
json |> Poison.decode(%{as: %Stuff{}, keys: :atoms!}) |> something_with
end
Any thoughts?
Not sure you want to, but you could also use an ecto embedded schema definition, which would allow you to just cast the json into a “structure” (including type conversion)
Surely ecto provides some very nice functionality that could be used in this context.
Also I would never persist anything without using ecto, embedded or otherwise.
I was thinking about defining embedded modules with structs inside a module that uses Phoenix.Channel.
The structs would tell me about the kind of stuff coming in and the function definitions would be short.
So it’s more about clarity than anything else.
I must be missing something - why does Poison
have get involved in to_struct
? I suspect it’s because you really want to use the Poison.Decode.transform/2
functionality.
Given that the transform process relies on empty structs (possibly nested) I’d be tempted to just code the necessary boilerplate conversions without Poison
- that code can go into a separate module from the struct
definition. If there is an opportunity to eliminate the boilerplate in the future, great - otherwise and move on …
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.