mbuhot

mbuhot

Validating constructor pattern?

Is there a convention/idiom for defining structs that require validation on construction?

I notice the standard library types Date and Time define a new function that returns an error tuple when the inputs are invalid:

iex> Date.new(0,0,0)
{:error, :invalid_date}

Is there any downside to declaring structs as ecto embedded_schema with a new/1 function that returns an error tuple? eg:

defmodule MyStruct do
  use Ecto.Schema
  alias Ecto.Changeset

  @primary_key false
  embedded_schema do 
    field :a, :integer
    field :b, :string
  end

  def new(attrs) do
    case cs = changeset(%MyStruct{}, attrs) do
      %{valid?: true} -> {:ok, Changeset.apply_changes(cs)}
      _ -> {:error, changeset_errors(cs)}
    end
  end

  defp changeset(data = %MyStruct{}, attrs) do
    data
    |> Changeset.cast(attrs, [:a, :b])
    |> Changset.validate_required([:a, :b])
    |> Changeset.validate_format(:b, ~r/@/)
  end

  defp changeset_errors(changeset) do
    Changeset.traverse_errors(changeset, fn {msg, opts} ->
      Enum.reduce(opts, msg, fn {key, value}, acc ->
        String.replace(acc, "%{#{key}}", to_string(value))
      end)
    end)
  end
end

Most Liked

Qqwy

Qqwy

TypeCheck Core Team

Indeed, using a new function, combined with declaring the struct’s type as @opaque (as hint that people should not construct it directly and in many cases not pattern match on it directly either) is the convention that is used for this. As for the return value in case of an error, there is no convention for this, (I have seen all of %MyStruct{} | nil, %MyStruct{} | {:error, reason} and {:ok, %MyStruct{}} | {:error, reason}; I like the third the most myself, but it depends on the situation).

As for the example with the Ecto changeset: I am not sure this is the proper way. It feels like you are partially re-inventing the logic that changesets themselves provide, but I am not immediately sure how to improve on it.

It feels more natural to me to have MyStruct.new() return a version of the struct with sensible default values (without any filled in ‘attrs’) and explicitly fill in the changeset with attrs at the place where you use it, i.e:

defmodule MyStructContext do
  def create_mystruct(attrs) do
    MyStruct.new
    |> creation_changeset(attrs)
    |> Repo.insert()
  end

  def creation_changeset(mystruct = %MyStruct, attrs) do
    mystruct
    |> MyStruct.common_changeset(attrs) # Ensures that invariants of MyStruct are never invalidated.
    |> Changeset.cast(attrs, [:someting, :only_applicable_on_creation])
    |> Changeset.validate_required([:something, :only_applicable_on_creation])
  end
end
ashneyderman

ashneyderman

I use validate_change for the embedded maps, which takes function as validator. This way you can compose validations pretty much for any imaginable structure.

Where Next?

Popular in Questions Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New

Other popular topics Top

Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 42576 114
New

We're in Beta

About us Mission Statement