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?

4 Likes

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}
2 Likes

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

6 Likes

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.

11 Likes

I like this idea, thanks!

Well, that’s the thing. I like to imagine the different OTP applications which compose my Elixir applicaiton as isolated modules, that only interact with each other through explicilty enabled interfaces (just like in a MicroService architecture, but all within Elixir).

Having the same exact keys for both looks like an overkill, but is actually enforcing a way to decouple the pure application logic from the DB and persistance layer. If I just use the DB resource, I’m coupling 100% one application with the other - and I really want to avoid that.

2 Likes

I’ve used Remodel with success.

It’s useful if your fields are different but there’s someoverlap (you specify the fields you want, and if they’re in the input struct they simply transfer), if there’s some direct field to field translation, and / or some more complex logic for computing the new fields.

1 Like

@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.

5 Likes

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!

1 Like

I was searching for the same decoupling, defining in my umbrella project an ADT Interface (Abstract Data Type).

And using direct Phoenix PubSub and RPC for data exchange through the ADTs types.

Works perfectly, just dont know yet how that will perform when in Prod Env.