diff --git a/lib/typespec.ex b/lib/typespec.ex index 240b2ac..15909a7 100644 --- a/lib/typespec.ex +++ b/lib/typespec.ex @@ -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 diff --git a/test/strukt_test.exs b/test/strukt_test.exs index 02c66a6..b423f1e 100644 --- a/test/strukt_test.exs +++ b/test/strukt_test.exs @@ -558,6 +558,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} -> diff --git a/test/support/defstruct_fixtures.ex b/test/support/defstruct_fixtures.ex index 66cb8f2..6565641 100644 --- a/test/support/defstruct_fixtures.ex +++ b/test/support/defstruct_fixtures.ex @@ -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 @@ -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" @@ -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]"]