A utility library for Ecto


Keywords
ecto, elixir, elixir-phoenix, functional-programming, phoenix
License
Apache-2.0

Documentation

EctoMorph

Module Version Hex Docs Total Download License Last Updated

EctoMorph morphs your Ecto capabilities into the s t r a t o s p h e r e !

Parse incoming data into custom structs, then validate it.

Usually you have to do something like this:

defmodule Embed do
  use Ecto.Schema

  embedded_schema do
    field(:bar, :string)
  end
end

defmodule Test do
  use Ecto.Schema

  embedded_schema do
    field(:thing, :string)
    embeds_one(:embed, Embed)
  end

Ecto.Changeset.cast(%Test{}, %{"thing" => "foo", "embed" => %{"bar"=> "baz"}}, [:thing])
|> Ecto.Changeset.cast_embed(:embed)

Now we can do this:

data = %{"thing" => "foo", "embed" => %{"bar"=> "baz"}}
EctoMorph.cast_to_struct(data, Test)

# or
data = %{"thing" => "foo", "embed" => %{"bar"=> "baz"}}
EctoMorph.cast_to_struct(data, Test, [:thing, embed: [:bar]])

# The data can also be a struct so this would work:
data = %Test{thing: "foo", embed: %Embed{bar: "baz"}}
EctoMorph.cast_to_struct(data, Test, [:thing, embed: [:bar]])

# So would this:
data = %{"thing" => "foo", "embed" => %{"bar"=> "baz"}}
EctoMorph.cast_to_struct(data, %Test{}, [:thing, embed: [:bar]])

# Changes can even be a different struct, if it
# has overlapping keys they will be casted as expected:

defmoule OtherStruct do
  defstruct [:thing, :embed]
end

data = %OtherStruct{thing: "foo", embed: %{"bar"=> "baz"}}
EctoMorph.cast_to_struct(data, %Test{}, [:thing, embed: [:bar]])

Or something like this:

with {:ok, %{status: 200, body: body}} <- HTTPoison.get("mygreatapi.co.uk") do
  EctoMorph.cast_to_struct(Jason.decode!(body), User)
end

We can also whitelist fields to cast / update:

data = %{"thing" => "foo", "embed" => %{"bar"=> "baz"}}
EctoMorph.cast_to_struct(data, Test, [:thing])

data = %{"thing" => "foo", "embed" => %{"bar"=> "baz"}}
EctoMorph.cast_to_struct(data, Test, [:thing, embed: [:bar]])

Sometimes it makes sense to update a struct we have retrieved from the database with data from our response. We can do that like so:

def update(data) do
  # This will update the db struct with the data passed in, then update the db.
  MyRepo.get!(MySchema, 10)
  |> EctoMorph.update_struct(data)
  |> MyRepo.update!()
end

Validations

Often you'll want to do some validations, that's easy:

(
  %{"thing" => "foo", "embed" => %{"bar"=> "baz"}}
  |> EctoMorph.generate_changeset(Test, [:thing])
  |> Ecto.Changeset.validate_required([:thing])
  |> EctoMorph.into_struct()
)

# or
(
  %{"thing" => "foo", "embed" => %{"bar"=> "baz"}}
  |> EctoMorph.generate_changeset(Test, [:thing])
  |> Ecto.Changeset.validate_change(...)
  |> Repo.insert!
)

Valiating Nested Changesets

Easily the coolest feature, say you have nested changesets via embeds or has_one/many, you can now specify a path to a changeset and specify a validation function for the changeset(s) at the end of that path. If your path ends at a list of changesets (because your model has a has_many relation for example), each of those changesets will be validated.

%{"thing" => "foo", "embed" => %{"bar"=> "baz"}}
|> EctoMorph.generate_changeset(Test)
|> EctoMorph.validate_nested_changeset([:embed], &MyEmbed.validate/1)

# or
json = %{
  "has_many" => [
    %{"steamed_hams" => [%{"pickles" => 1}, %{"pickles" => 2}]},
    %{"steamed_hams" => [%{"pickles" => 1}]},
    %{"steamed_hams" => [%{"pickles" => 4}, %{"pickles" => 5}]}
  ]
}

# Here each of the steamed_hams above will have their pickle count validated:

EctoMorph.generate_changeset(json, MySchema)
|> EctoMorph.validate_nested_changeset([:has_many, :steamed_hams], fn changeset ->
  Ecto.Changeset.validate_number(changeset, :pickles, greater_than: 3)
end)

Other abilities include creating a map from an ecto struct, dropping optional fields if you decide to:

EctoMorph.map_from_struct(%Test{})
%{foo: "bar", updated_at: ~N[2000-01-01 23:00:07], inserted_at: ~N[2000-01-01 23:00:07], id: 10}

EctoMorph.map_from_struct(%Test{}, [:exclude_timestamps])
%{foo: "bar", id: 10}

EctoMorph.map_from_struct(%Test{}, [:exclude_timestamps, :exclude_id])
%{foo: "bar"}

and being able to filter some data by the fields in the given schema:

defmodule Test do
  use Ecto.Schema

  embedded_schema do
    field(:random, :string)
  end
end

EctoMorph.filter_by_schema_fields(%{random: "data", more: "fields"}, Test)
%{random: "data"}

You can even deep filter:

defmodule OtherThing do
  use Ecto.Schema
  @primary_key false
  embedded_schema do
    field(:id, :integer)
  end
end

defmodule Test do
  use Ecto.Schema

  embedded_schema do
    field(:random, :string)
    embeds_one(:other_thing, OtherThing)
  end
end

data = %{
  random: "data",
  more: "fields",
  __meta__: "stuff",
  other_thing: %{id: 1, ignored: "field"}
}

EctoMorph.deep_filter_by_schema_fields(data, Test)
%{random: "data", other_thing: %{id: 1}}

Deep filtering will keep virtual fields, relations and through relations. That means you can use it to create a map of the struct fields without ecto metadata if you filter it by itself:

data = %Test{
  random: "data",
  more: "fields",
  __meta__: "stuff",
  other_thing: %OtherThing{id: 1, ignored: "field"}
}

EctoMorph.deep_filter_by_schema_fields(data, Test)
%{random: "data", other_thing: %{id: 1}}

Check out the docs for more examples and specifics

Contributing

NB Set the MIX_ENV to :docs when publishing the package. This will ensure that modules inside test/support wont get their documentation published with the library (as they are included in the :dev env).

MIX_ENV=docs mix hex.publish

Installation

The package can be installed by adding :ecto_morph to your list of dependencies in mix.exs:

def deps do
  [
    {:ecto_morph, "~> 0.1.29"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/ecto_morph.

Copyright and License

Copyright (c) 2019 Adam Lancaster

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.