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
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.
LostKobrakai
Another way would be using struct(MyApp.Foo, Map.from_struct(db_struct))
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
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
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}








