How to get struct from map - elixir?

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

Kernel.with/1

1 Like

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)

1 Like

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 :man_shrugging: and move on …

1 Like

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.

2 Likes

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 :wink:

@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

?

2 Likes

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
1 Like

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.

2 Likes

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.

2 Likes

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.

1 Like

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. :+1: