Skip to content
Merged
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
74 changes: 54 additions & 20 deletions lib/typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -147,30 +147,64 @@ defmodule Strukt.Typespec do
compose_call(Ecto.UUID, :t, [])

mod ->
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,
[]
)
if defines_module_type?(mod) do
compose_call(module_alias_ast(mod), :t, [])
else
_ ->
# Module is unable to be loaded, either due to compiler deadlock, or because
# the module name we have is an alias, or perhaps just plain wrong, so we can't
# assume anything about its type
primitive(:any)
# Fall back to the underlying Ecto type when available, rather than
# relying on debug info being present in the BEAM to discover `t/0`.
ecto_type_to_type_name(mod) || primitive(:any)
end
end
end

defp type_to_type_name(_), do: primitive(:any)

defp defines_module_type?(mod) do
module_defines_type?(mod) or docs_define_type?(mod) or beam_defines_type?(mod)
end

defp module_defines_type?(mod) do
Module.defines_type?(mod, {:t, 0})
rescue
ArgumentError -> false
end

defp docs_define_type?(mod) do
case Code.fetch_docs(mod) do
{:docs_v1, _, _, _, _, _, docs} ->
Enum.any?(docs, &match?({{:type, :t, 0}, _, _, _, _}, &1))

_ ->
false
end
end

defp beam_defines_type?(mod) do
case Code.Typespec.fetch_types(mod) do
{:ok, mod_types} ->
Enum.any?(
mod_types,
&match?({kind, {:t, _, args}} when kind in [:type, :opaque] and length(args) == 0, &1)
)

_ ->
false
end
end

defp ecto_type_to_type_name(mod) do
with {:module, _} <- Code.ensure_loaded(mod),
true <- function_exported?(mod, :type, 0) do
mod.type()
|> type_to_type_name()
else
_ -> nil
end
rescue
_ -> nil
end

defp module_alias_ast(mod) do
{:__aliases__, [alias: false], Enum.map(Module.split(mod), &String.to_atom/1)}
end
end
20 changes: 20 additions & 0 deletions test/strukt_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,26 @@ defmodule Strukt.Test do
Fixtures.CustomEctoTypeTypeSepc.expected_type_spec_ast_str()
end

test "custom ecto type without beam debug info" do
require Fixtures.NoDebugInfoCustomEctoTypeTypeSpec

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

defp changeset_errors(%Ecto.Changeset{} = cs) do
cs
|> Ecto.Changeset.traverse_errors(fn {msg, opts} ->
Expand Down
44 changes: 44 additions & 0 deletions test/support/defstruct_fixtures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,32 @@ defmodule Custom.EctoType do
def dump(_), do: :error
end

defmodule Custom.NoDebugInfoEctoType do
@moduledoc "Custom Ecto Type without BEAM debug info for test"
@compile {:debug_info, false}
@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 @@ -116,6 +142,24 @@ defmodule Strukt.Test.Fixtures do
end
end

defmodule NoDebugInfoCustomEctoTypeTypeSpec do
use Strukt

@primary_key false
defstruct do
field(:uri, Custom.NoDebugInfoEctoType)
end

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

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

Expand Down
Loading