IvanRublev / Domo

A library to validate values of nested structs with their type spec t() and associated precondition functions

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Default values vs custom constructors

tcoopman opened this issue · comments

This is a question, not a bug report.

I'm playing with Domo for the first time (in combination with TypedStruct) and it looks nice, but I'm immediately running into a use case that I don't see documented.

This is the code I wanted to port. Like you can see, my new functions generates a random token on construction. Can Domo do this as well? If so, how would I do it?

defmodule Foo do
  use TypedStruct

  typedstruct opaque: true do
    field :id, non_neg_integer, enforce: true
    field :token, String.t(), enforce: true
  end

  def new(id) do
    %Foo{id: id, token: random_string(8)}
  end

  defp random_string(length),
    do: :crypto.strong_rand_bytes(length) |> Base.encode64() |> binary_part(0, length)
end

Maybe I should not use Domo here, because I do the construction myself, but then I loose all other nice things as well.

Hey,
Thanks for your message. I've released version 1.5.8, where all Domo-generated constructor functions are overridable. So the injection of the default value can be done in the overridden constructor like the following:

defmodule Foo do
  use TypedStruct
  use Domo

  typedstruct opaque: true do
    field :id, non_neg_integer, enforce: true
    field :token, String.t(), enforce: true
  end

  # keyword list variant
  def new!([_|_] = fields) do
    super(Keyword.merge(fields, token: random_string(8)))
  end
  
  # argumented variant
  def new!(id) do
    super(id: id, token: random_string(8))
  end

  defp random_string(length),
    do: :crypto.strong_rand_bytes(length) |> Base.encode64() |> binary_part(0, length)
end

I made Domo to validate invariants for nested structs and declare them in shared kernels. Don't know if this is your use case specifically 😄 It'd be good to hear about how far you went with Domo! You can write me at https://twitter.com/LevviBraun

Thanks for the quick follow-up.
I've tried it and this is how my code looks now:

defmodule Foo do
  use TypedStruct
  use Domo

  @type id :: non_neg_integer()
  precond(id: &(&1 >= 1 && &1 <= 4))

  @type token :: String.t()
  precond(token: &validate_required/1)

  defp validate_required(token) when byte_size(token) == 0, do: {:error, "can't be empty string"}
  defp validate_required(_token), do: :ok

  @derive Jason.Encoder
  typedstruct opaque: true do
    @typedoc "A player"

    field :id, id, enforce: true, default: 1
    field :token, token, enforce: true, default: "invalid"
  end

  def new() do
    {:error, :not_allowed}
  end

  def new!() do
    raise "Not allowed"
  end

  def new!([_ | _] = fields) do
    id = Keyword.fetch!(fields, :id)
    super(id: id, token: random_string(8))
  end

  def new!(id) do
    super(id: id, token: random_string(8))
  end

  def new([_ | _] = fields) do
    id = Keyword.fetch!(fields, :id)
    super(id: id, token: random_string(8))
  end

  def new(id) do
    super(id: id, token: random_string(8))
  end

  defp random_string(length),
    do: :crypto.strong_rand_bytes(length) |> Base.encode64() |> binary_part(0, length)
end

Some things that I noticed:

  1. The defaults are still required, even though no constructor functions allow the struct to be build without defaults. Furthermore the defaults need to pass the preconditions.
  2. I need to override 6 variants of new

I see why this might be the case, and again, maybe Domo is not what I'm looking for.

How I'd like to write my code would be like this:

defmodule Foo do
  use TypedStruct
  use Domo

  @type id :: non_neg_integer()
  precond(id: &(&1 >= 1 && &1 <= 4))

  @type token :: String.t()
  precond(token: &validate_required/1)

  defp validate_required(token) when byte_size(token) == 0, do: {:error, "can't be empty string"}
  defp validate_required(_token), do: :ok

  @derive Jason.Encoder
  typedstruct opaque: true do
    @typedoc "A player"

    field :id, id, enforce: true
    field :token, token, enforce: true
  end

  def new(id) do
    %Foo{id: id, token: random_string(8)} |> Foo.ensure_type
  end

  defp random_string(length),
    do: :crypto.strong_rand_bytes(length) |> Base.encode64() |> binary_part(0, length)
end

So in this case I'd only use the ensure_type and precond to validate the structs without any of the constructors of Domo itself.
I know I can manually write this myself, but the precond macros of Domo are quite nice :-)

If this is not what you intend to do with the library, feel free to close this issue.

Hi,
Thanks for your message.
Cool, you found the way by making your own constructor!

The compile-time defaults validation is one of the possible ways to make invalid states impossible.

FYI: The feature can be switched off with skip_defaults: true option. It can be passed with use Domo, skip_defaults: true or set for the entire project by adding config :domo, skip_defaults: true into confix.exs.

I see you use Jason.Encoder, which seems is for encoding the struct into JSON.

In case, if you need the reverse operation to translate from JSON -> nested struct, you can have a look at Nestru which works for structs pretty much the same way by deriving the protocol.

@tcoopman, thanks again for this example.

I've added the https://github.com/IvanRublev/Domo#custom-constructor-function section to the Readme to make library users know how to implement it.