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