matiso

matiso

Idiomatic way to convert from one struct to another

My use case is the following. I have an umbrella application that clearly separates the DB access layer from the business logic. By doing this, I don’t want to directly expose the DB resources to the other applications.

Instead, what I want is to have an edge interface that will expose the DB resource, and a receiver that will do the conversion from the “DB resource” struct, to an internal POEXS (plain old Elixir struct).

I know it might be overcomplicating things, but I want to see how far I can get with this approach.

What bothers me is that I don’t see a way to directly transform StructA into StructB.

So if I have:

%DB.FooResource{name: "bar"}

And I’d like to convert it into:

%MyApp.Foo{name: "bar"}

What I’d need to do is to convert the first one into a map, and create a struct from that map.

data = db_resource |> Map.from_struct()
struct(MyApp.Foo, data)

A bit cumbersome in my opinion, especially if you get an array of those.

Is there a better way of doing this, or is it cumbersome, because it’s a wrong approach?

Most Liked

axelson

axelson

Scenic Core Team

I’d think that you’d probably want to create a MyApp.Foo.from_foo_resource/1 to encapsulate the knowledge of how to transform a %DB.FooResource{} into a %MyApp.Foo{} rather than potentially hard-coding that knowledge in multiple places. It might look something like:

defmodule MyApp.Foo do
  defstruct [:name]
  
  def from_foo_resource(%DB.FooResource{} = foo_resource) do
    struct(MyApp.Foo, Map.from_struct(foo_resource))
  end
end

Then if you have a list of %DB.FooResource{} you can do

resource_list = [%DB.FooResource{name: "a"}, %DB.FooResource{name: "b"}]
foo_list = Enum.map(resource_list, &MyApp.Foo.from_foo_resource/1)

Although if all you have is a one-to-one mapping this entire approach sounds like it is overkill (but it is an interesting experiment). I’d imagine if you’re combining multiple DB resources into one representation then something like this definitely makes sense. Or if you’re doing some type of CQRS system.

13
Post #4
LostKobrakai

LostKobrakai

Another way would be using struct(MyApp.Foo, Map.from_struct(db_struct))

anthonator

anthonator

@matiso we’re doing something similar with an umbrella app.

Here’s what we came up with.

defmodule Context.Primary.Helpers.Entity do
  def new(nil, _entity) do
    nil
  end

  def new(%_{} = struct, entity) do
    struct
    |> Map.from_struct()
    |> new(entity)
  end

  def new(data, entity) do
    struct(entity, data)
  end
end

Pretty straightforward. This does break down when you need to compose something from multiple sources.

vschroeder

vschroeder

We are doing the same here, so when retrieving data from DB (usually JSON), we like to wrap it in an internal struct that our modules will know about, but for other needs, we perform a conversion to another struct (passing data to an API, saving to another destination, outputting to the client, etc). To reduce verbosity, we expose a function from/1 on the destination struct, which pattern matches one or more source structs and, potentially, a raw JSON map.

For example:

defmodule Whatever.DB.User do
  defstruct [:id, :first_name, :last_name, :hash, :permissions]

  # This one is to create a struct from JSON map
  def from(%{
    "_id" => id,
    "firstName" => first_name,
    "lastName" => last_name,
    "hash" => hash,
    "permissions" => permissions,
  }) do
    %__MODULE__{
      id: id,
      first_name: first_name,
      last_name: last_name,
      hash: hash,
      permissions: permissions,
    }
  end
end

defmodule Whatever.Public.User do
  defstruct [:id, :full_name]

  def from(%Whatever.DB.User{
    id: id,
    first_name: first_name,
    last_name: last_name,
  }) do
    %__MODULE__{
      id: id,
      full_name: "#{first_name} #{last_name}",
    }
  end
end

An so on. The pattern matching is the coolest thing ever for this kind of structure, as it does not only the matching to the correct struct, but also validates the incoming structure. This can, of course, get much longer and as complex as we need, with default values, partial matches and such.

It’s a bit repetitive and quite some boilerplate, but when using, it’s very nice:

alias Whatever.DB.User, as: DBUser
alias Whatever.Public.User, as: PublicUser

user = DBUser.from fetch_json_from_db_somehow
public_user = PublicUser.from user

This gets even better when implementing protocols for JSON encoding/decoding!

OvermindDL1

OvermindDL1

Probably this. ^.^;

But still, do they all have identical fields? If so then you can just swap the __struct__ value on it:

iex(15)> defmodule Tester1 do defstruct name: 1 end
{:module, Tester1,
 <<70, 79, 82, 49, 0, 0, 5, 40, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 143,
   0, 0, 0, 13, 14, 69, 108, 105, 120, 105, 114, 46, 84, 101, 115, 116, 101,
   114, 49, 8, 95, 95, 105, 110, 102, 111, 95, ...>>, %Tester1{name: 1}}
iex(16)> defmodule Tester2 do defstruct name: 2 end
{:module, Tester2,
 <<70, 79, 82, 49, 0, 0, 5, 40, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 143,
   0, 0, 0, 13, 14, 69, 108, 105, 120, 105, 114, 46, 84, 101, 115, 116, 101,
   114, 50, 8, 95, 95, 105, 110, 102, 111, 95, ...>>, %Tester2{name: 2}}
iex(17)> t = %Tester1{}
%Tester1{name: 1}
iex(18)> tt = %{t | __struct__: Tester2}
%Tester2{name: 1}

That only works if they have identical fields though, but it is the fastest way for sure.

Be sure they match perfectly, if their fields don’t match or the struct name is wrong then things start to fail in interesting ways:

iex(19)> %{t | __struct__: Tester3}
%{__struct__: Tester3, name: 1}

Where Next?

Popular in Questions Top

Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
Werner
Hi, I’m using Ubuntu 18.04 and after updating to OTP-24.0 yesterday i have this warning when I run “mix local.hex”: 14:57:30.512 [warn] ...
New
Kagamiiiii
Student &amp; New to elixir. Nice language. I want to convert a english character, e.g. “a”, which is stored in a variable, to it’s asci...
New
gazoon
I want to know absolute current module path. In python i could do that: os.path.abspath(__file__) Does elixir have anything similar?
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
peerreynders
Manning 2016 Halloween weekend sale via Deal of the Day Friday, October 28 - Half off all MEAPs - code WM102816LT Saturday, October 29 ...
326 29600 154
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
AstonJ
You’re a programmer, so you don’t need spoon feeding with the conventional drivel about “this is an integer.” No. You need to know what’s...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
vac
Hi, I'm quite new in Elixir and I'm trying to format a string to a PEM format. I have the certificate value like MIIDBTCCAe2...... and ...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New

We're in Beta

About us Mission Statement