Skip to content

Commit

Permalink
Use Macro.to_string/1 instead of parsing specs by "hand" (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blatts12 authored Oct 17, 2023
1 parent d6c11f7 commit 1310f05
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 55 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Add the following to your `mix.exs` file:
```elixir
defp deps do
[
{:contexted, "~> 0.1.10"}
{:contexted, "~> 0.1.11"}
]
end
```
Expand Down
30 changes: 29 additions & 1 deletion lib/contexted/delegator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ defmodule Contexted.Delegator do
|> Enum.map(fn {name, arity} ->
args = ModuleAnalyzer.generate_random_function_arguments(arity)
doc = ModuleAnalyzer.get_function_doc(functions_docs, name, arity)
spec = ModuleAnalyzer.get_function_spec(functions_specs, name, arity, module)
spec = ModuleAnalyzer.get_function_spec(functions_specs, name, arity)

{name, arity, args, doc, spec}
end)
Expand All @@ -85,9 +85,37 @@ defmodule Contexted.Delegator do
end
end)

types =
case Code.Typespec.fetch_types(module) do
{:ok, types} ->
Enum.map(types, fn
{type_of_type, type} ->
Code.Typespec.type_to_quoted(type)
|> add_ast_for_type(type_of_type)

_ ->
nil
end)

_ ->
[]
end

# Combine the generated delegates into a single AST
quote do
(unquote_splicing(types))
(unquote_splicing(delegates))
end
end

@spec add_ast_for_type(tuple(), atom()) :: tuple()
defp add_ast_for_type(ast, type_of_type) do
{:@, [context: Elixir, imports: [{1, Kernel}]],
[
{type_of_type, [context: Elixir],
[
ast
]}
]}
end
end
71 changes: 19 additions & 52 deletions lib/contexted/module_analyzer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ defmodule Contexted.ModuleAnalyzer do
The `Contexted.ModuleAnalyzer` defines utils functions that analyze and extract information from other modules.
"""

@module_prefix "Elixir."

@doc """
Fetches the `@doc` definitions for all functions within the given module.
"""
Expand Down Expand Up @@ -37,14 +35,14 @@ defmodule Contexted.ModuleAnalyzer do
Finds and returns the `@spec` definition in string format for the specified function name and arity.
Returns `nil` if the function is not found in the specs.
"""
@spec get_function_spec([tuple()], atom(), non_neg_integer(), module()) :: String.t() | nil
def get_function_spec(specs, function_name, arity, module) do
@spec get_function_spec([tuple()], atom(), non_neg_integer()) :: String.t() | nil
def get_function_spec(specs, function_name, arity) do
# Find the spec tuple in the specs
spec = find_spec(specs, function_name, arity)

# If spec is found, build the spec expression
if spec do
build_spec(spec, module)
build_spec(spec)
else
nil
end
Expand Down Expand Up @@ -89,54 +87,23 @@ defmodule Contexted.ModuleAnalyzer do
end)
end

@spec build_spec(tuple(), module()) :: String.t()
defp build_spec({{function_name, _arity}, spec}, module) do
{:type, _, :fun, [arg_types, return_type]} = hd(spec)
arg_types_string = format_arg_types(arg_types, module)
return_type_string = format_type(return_type, module)

function_with_args = "#{function_name}(#{arg_types_string})"
return_value = return_type_string

"@spec #{function_with_args} :: #{return_value}"
end

@spec format_arg_types(tuple(), module()) :: String.t()
defp format_arg_types({:type, _, :product, []}, _module), do: ""

defp format_arg_types({:type, _, :product, arg_types}, module) do
Enum.map_join(arg_types, ",", &format_type(&1, module))
end

@spec format_type(tuple(), module()) :: String.t()
defp format_type({:type, _, :union, types}, module) do
Enum.map_join(types, " | ", &format_type(&1, module))
end

defp format_type({:type, _, type_name, _}, _module), do: "#{type_name}()"

defp format_type({:user_type, _, atom, _}, module) do
"#{Atom.to_string(module)}.#{Atom.to_string(atom)}()"
end

defp format_type({:atom, _, atom}, _module) do
stringed_atom = Atom.to_string(atom)

if is_module(stringed_atom) do
stringed_atom
else
":#{stringed_atom}"
end
@spec build_spec(tuple()) :: String.t()
defp build_spec({{function_name, _arity}, specs}) do
Enum.map_join(specs, "\n", fn spec ->
Code.Typespec.spec_to_quoted(function_name, spec)
|> add_spec_ast()
|> Macro.to_string()
end)
end

defp format_type({:remote_type, _, [{:atom, _, module}, {:atom, _, type}, _list]}, _module) do
if module == :elixir do
"#{type}()"
else
"#{module}.#{type}()"
end
@spec add_spec_ast(tuple()) :: tuple()
defp add_spec_ast(ast) do
{:@, [context: Elixir, imports: [{1, Kernel}]],
[
{:spec, [context: Elixir],
[
ast
]}
]}
end

@spec is_module(String.t()) :: boolean()
defp is_module(stringed_atom), do: String.starts_with?(stringed_atom, @module_prefix)
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Contexted.MixProject do
app: :contexted,
description:
"Contexted is an Elixir library designed to streamline the management of complex Phoenix contexts in your projects, offering tools for module separation, subcontext creation, and auto-generating CRUD operations for improved code maintainability.",
version: "0.1.10",
version: "0.1.11",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps(),
Expand Down

0 comments on commit 1310f05

Please sign in to comment.