Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix custom type spec #21

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions lib/typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -138,30 +138,30 @@ defmodule Strukt.Typespec do
defp type_to_type_name(:date),
do: compose_call(Date, :t, [])

defp type_to_type_name({:__aliases__, _, parts} = ast) do
case Module.concat(parts) do
defp type_to_type_name(mod) when is_atom(mod) and not is_nil(mod) do
case mod do
Ecto.Enum ->
primitive(:atom)

Ecto.UUID ->
primitive(:string)
compose_call(Ecto.UUID, :t, [])

mod ->
with {:module, _} <- Code.ensure_compiled(mod) do
try do
if Kernel.Typespec.defines_type?(mod, {:t, 0}) do
compose_call(ast, :t, [])
else
# No t/0 type defined, so fallback to any/0
primitive(:any)
end
rescue
ArgumentError ->
# We shouldn't hit this branch, but if Elixir can't find module metadata
# during defines_type?, it raises ArgumentError, so we handle this like the
# other pessimistic cases
primitive(:any)
end
with {:module, _} <- Code.ensure_compiled(mod),
{:ok, mod_types} <- Code.Typespec.fetch_types(mod),
t0 when not is_nil(t0) <-
Enum.find(
mod_types,
&match?(
{kind, {:t, _, args}} when kind in [:type, :opaque] and length(args) == 0,
&1
)
) do
compose_call(
{:__aliases__, [alias: false], Enum.map(Module.split(mod), &String.to_atom/1)},
:t,
[]
)
else
_ ->
# Module is unable to be loaded, either due to compiler deadlock, or because
Expand Down
20 changes: 20 additions & 0 deletions test/strukt_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,26 @@ defmodule Strukt.Test do
Fixtures.EmbedsOneTypeSpec.expected_type_spec_ast_str()
end

test "custom ecto type" do
require Fixtures.CustomEctoTypeTypeSepc

assert inspect(
Strukt.Typespec.generate(%Strukt.Typespec{
caller: Strukt.Test.Fixtures.CustomEctoTypeTypeSepc,
fields: [:uri],
info: %{
uri: %{
type: :field,
value_type: Custom.EctoType,
required: true
}
},
embeds: []
})
) ==
Fixtures.CustomEctoTypeTypeSepc.expected_type_spec_ast_str()
end

defp changeset_errors(%Ecto.Changeset{} = cs) do
cs
|> Ecto.Changeset.traverse_errors(fn {msg, opts} ->
Expand Down
48 changes: 47 additions & 1 deletion test/support/defstruct_fixtures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,31 @@ defmodule Aux do
end
end

defmodule Custom.EctoType do
@moduledoc "Custom Ecto Type for test"
@type t() :: URI.t()

use Ecto.Type
def type, do: :map

def cast(uri) when is_binary(uri), do: {:ok, URI.parse(uri)}
def cast(%URI{} = uri), do: {:ok, uri}

def cast(_), do: :error

def load(data) when is_map(data) do
data =
for {key, val} <- data do
{String.to_existing_atom(key), val}
end

{:ok, struct!(URI, data)}
end

def dump(%URI{} = uri), do: {:ok, Map.from_struct(uri)}
def dump(_), do: :error
end

defmodule Strukt.Test.Fixtures do
use Strukt

Expand Down Expand Up @@ -72,6 +97,25 @@ defmodule Strukt.Test.Fixtures do
end
end

defmodule CustomEctoTypeTypeSepc do
use Strukt

@primary_key false
defstruct do
field(:uri, Custom.EctoType)
field(:foobar, :string)
end

defmacro expected_type_spec_ast_str do
quote context: __MODULE__ do
@type t :: %__MODULE__{
uri: Custom.EctoType.t()
}
end
|> inspect()
end
end

defmodule CustomFields do
@moduledoc "This module represents the params keys are not snake case"

Expand Down Expand Up @@ -327,7 +371,9 @@ defmodule Strukt.Test.Fixtures do
defstruct ValidateSets do
@moduledoc "This module exercises validations based on set membership"

field(:one_of, :string, one_of: [values: ["a", "b", "c"], message: "must be one of [a, b, c]"])
field(:one_of, :string,
one_of: [values: ["a", "b", "c"], message: "must be one of [a, b, c]"]
)

field(:none_of, :string,
none_of: [values: ["a", "b", "c"], message: "cannot be one of [a, b, c]"]
Expand Down
Loading