Skip to content

Commit

Permalink
To typespec (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
visciang authored Oct 15, 2022
1 parent 6ad9c45 commit 3be70de
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:
image: elixir:1.14.0

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3

- name: Retrieve Cached Dependencies
uses: actions/cache@v2
uses: actions/cache@v3
id: mix-cache
with:
path: |
Expand Down
20 changes: 20 additions & 0 deletions lib/dataspecs/schema/formatter.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule DataSpecs.Schema.Formatter do
@moduledoc """
Formatter.
"""

alias DataSpecs.Schema.Quoted
alias DataSpecs.Schema.Type

@doc """
Convert a schema to its typespec representation.
"""
@spec to_typespec_string(Type.t(), width :: pos_integer()) :: String.t()
def to_typespec_string(%Type{} = t, width \\ 80) do
t
|> Quoted.from_schema()
|> Code.quoted_to_algebra()
|> Inspect.Algebra.format(width)
|> IO.iodata_to_binary()
end
end
129 changes: 129 additions & 0 deletions lib/dataspecs/schema/quoted.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
defmodule DataSpecs.Schema.Quoted do
@moduledoc false

alias DataSpecs.Schema.Type

@doc """
Convert a schema to a quoted representation.
"""
@spec from_schema(Type.t()) :: Macro.t()
def from_schema(%Type{} = t) do
from_schema_(t)
end

@spec from_schema_(Type.t() | Type.type()) :: Macro.t()
defp from_schema_(%Type{} = t) do
q_type_vars = Enum.map(t.vars, fn %Type.Var{id: id} -> {id, [], Elixir} end)
type_module = t.module |> Module.split() |> Enum.map(&String.to_atom/1)

{:@, [],
[
{t.visibility, [],
[
{:"::", [],
[
{{:., [], [{:__aliases__, [alias: false], type_module}, t.id]}, [], q_type_vars},
from_schema_(t.type)
]}
]}
]}
end

defp from_schema_(%Type.Literal.Atom{} = t) do
t.value
end

defp from_schema_(%Type.Literal.Integer{} = t) do
t.value
end

defp from_schema_(%Type.Builtin{} = t) do
{t.id, [], []}
end

defp from_schema_(%Type.Bitstring{} = t) do
case {t.unit, t.size} do
{0, 0} ->
{:<<>>, [], []}

{0, size} ->
{:<<>>, [], [{:"::", [], [{:_, [], Elixir}, size]}]}

{unit, 0} ->
{:<<>>, [],
[
{:"::", [],
[
{:_, [], Elixir},
{:*, [], [{:_, [], Elixir}, unit]}
]}
]}

{size, unit} ->
{:<<>>, [],
[
{:"::", [], [{:_, [], Elixir}, unit]},
{:"::", [],
[
{:_, [], Elixir},
{:*, [], [{:_, [], Elixir}, size]}
]}
]}
end
end

defp from_schema_(%Type.Range{} = t) do
{:.., [], [t.lower, t.upper]}
end

defp from_schema_(%Type.Var{} = t) do
{t.id, [], Elixir}
end

defp from_schema_(%Type.Union{} = t) do
case t.of do
[a, b] -> {:|, [], [from_schema_(a), from_schema_(b)]}
[a | rest] -> {:|, [], [from_schema_(a), from_schema_(%Type.Union{of: rest})]}
end
end

defp from_schema_(%Type.List{} = t) do
case t.cardinality do
0 -> []
:+ -> [from_schema_(t.of), {:..., [], Elixir}]
_ -> [from_schema_(t.of)]
end
end

defp from_schema_(%Type.Tuple{} = t) do
case t.cardinality do
:* -> {:tuple, [], []}
2 -> t.of |> Enum.map(&from_schema_/1) |> List.to_tuple()
_ -> {:{}, [], Enum.map(t.of, &from_schema_/1)}
end
end

defp from_schema_(%Type.Map{} = t) do
kv =
Enum.map(t.of, fn {%Type.Map.Key{} = key, value} ->
if key.required? do
{from_schema_(key.type), from_schema_(value)}
else
{{:optional, [], [from_schema_(key.type)]}, from_schema_(value)}
end
end)

{:%{}, [], kv}
end

defp from_schema_(%Type.Ref{} = t) do
q_type_params = Enum.map(t.params, &from_schema_/1)
type_module = t.module |> Module.split() |> Enum.map(&String.to_atom/1)

{{:., [], [{:__aliases__, [alias: false], type_module}, t.id]}, [], q_type_params}
end

defp from_schema_(%Type.Unsupported{}) do
:__unsupported__
end
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule DataSpecs.Mixfile do
[
app: :dataspecs,
name: "dataspecs",
version: "2.0.0",
version: "2.1.0",
elixir: "~> 1.7",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
Expand Down
138 changes: 138 additions & 0 deletions test/quoted_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
defmodule Test.DataSpecs.Schema.Quoted do
use ExUnit.Case, async: true

alias DataSpecs.Schema

@sample_type Test.DataSpecs.SampleType

setup_all do
schemas =
@sample_type
|> Schema.load()
|> Map.new(fn %Schema.Type{} = t ->
{{@sample_type, t.id, length(t.vars)}, t}
end)

[schemas: schemas]
end

test "literals", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_literal_atom, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_literal_atom() :: :a"

schema = Map.fetch!(schemas, {@sample_type, :t_literal_integer, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_literal_integer() :: 1"
end

test "builtin", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_any, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_any() :: any()"
end

test "bitstring", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_empty_bitstring, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_empty_bitstring() :: <<>>"

schema = Map.fetch!(schemas, {@sample_type, :t_bitstring_0, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_bitstring_0() :: <<_::4>>"

schema = Map.fetch!(schemas, {@sample_type, :t_bitstring_1, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_bitstring_1() :: <<_::_*4>>"

schema = Map.fetch!(schemas, {@sample_type, :t_bitstring_2, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_bitstring_2() :: <<_::16, _::_*4>>"
end

test "range", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_range, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_range() :: 1..10"
end

test "union", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_union_0, 1})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_union_0(x) :: x | atom() | integer()"
end

test "list", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_empty_list, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_empty_list() :: []"

schema = Map.fetch!(schemas, {@sample_type, :t_list, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_list() :: [atom()]"

schema = Map.fetch!(schemas, {@sample_type, :t_nonempty_list_0, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_nonempty_list_0() :: [any(), ...]"
end

test "tuple", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_empty_tuple, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_empty_tuple() :: {}"

schema = Map.fetch!(schemas, {@sample_type, :t_tuple, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_tuple() :: {integer(), integer()}"

schema = Map.fetch!(schemas, {@sample_type, :t_tuple_any_size, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_tuple_any_size() :: tuple()"
end

test "map", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_empty_map, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_empty_map() :: %{}"

schema = Map.fetch!(schemas, {@sample_type, :t_map_0, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_map_0() :: %{required_key: integer()}"

schema = Map.fetch!(schemas, {@sample_type, :t_map_5, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_map_5() :: %{optional(integer()) => atom()}"
end

test "ref", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_mapset, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_mapset() :: MapSet.t(integer())"
end

test "unsupported", %{schemas: schemas} do
schema = Map.fetch!(schemas, {@sample_type, :t_unsupported, 0})

assert Schema.Formatter.to_typespec_string(schema) ==
"@type Test.DataSpecs.SampleType.t_unsupported() :: :__unsupported__"
end
end

0 comments on commit 3be70de

Please sign in to comment.