Skip to content

Commit c191e78

Browse files
committed
test: add guides tests
1 parent 9c6f340 commit c191e78

10 files changed

+172
-17
lines changed

guides/plugins/derive_enumerable.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Let's start!
88

99
## Implementation
1010
```elixir
11-
defmodule MyPlugin do
11+
defmodule Guides.Plugins.Enumerable do
1212
use TypedStructor.Plugin
1313

1414
@impl TypedStructor.Plugin
@@ -40,11 +40,11 @@ end
4040
defmodule User do
4141
use TypedStructor
4242

43-
typed_structor do
44-
plugin MyPlugin
43+
typed_structor enforce: true do
44+
plugin Guides.Plugins.Enumerable
4545

46-
field :name, String.t(), enforce: true
47-
field :age, integer(), enforce: true
46+
field :name, String.t()
47+
field :age, Integer.t()
4848
end
4949
end
5050
```

guides/plugins/derive_jason.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ generate the `Jason.Encoder` implementation for the struct.
55

66
## Implementation
77
```elixir
8-
defmodule MyPlugin do
8+
defmodule Guides.Plugins.Jason do
99
use TypedStructor.Plugin
1010

1111
@impl TypedStructor.Plugin
@@ -29,10 +29,10 @@ end
2929
defmodule User do
3030
use TypedStructor
3131

32-
typed_structor do
33-
plugin MyPlugin
32+
typed_structor enforce: true do
33+
plugin Guides.Plugins.Jason
3434

35-
field :name, String.t(), enforce: true
35+
field :name, String.t()
3636
end
3737
end
3838
```

guides/plugins/primary_key_and_timestamps.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This plugin use `c:TypedStructor.Plugin.before_definition/2` callback to
77
inject the primary key and timestamps fields to the type definition.
88

99
```elixir
10-
defmodule MyApp.TypedStructor.Plugins.PrimaryKeyAndTimestamps do
10+
defmodule Guides.Plugins.PrimaryKeyAndTimestamps do
1111
use TypedStructor.Plugin
1212

1313
@impl TypedStructor.Plugin
@@ -33,14 +33,14 @@ end
3333
## Usage
3434

3535
```elixir
36-
defmodule MyApp.User do
36+
defmodule User do
3737
use TypedStructor
3838
use Ecto.Schema
3939

4040
# disable struct creation or it will conflict with the Ecto schema
4141
typed_structor define_struct: false do
4242
# register the plugin
43-
plugin MyApp.TypedStructor.Plugins.PrimaryKeyAndTimestamps
43+
plugin Guides.Plugins.PrimaryKeyAndTimestamps
4444

4545
field :name, String.t()
4646
field :age, integer(), enforce: true # There is always a non-nil value

guides/plugins/type_only_on_ecto_schema.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ set to `false` to prevent struct creation.
1313

1414
Here is the plugin(*feel free to copy and paste*):
1515
```elixir
16-
defmodule MyApp.TypedStructor.Plugins.TypeOnlyOnEctoSchema do
16+
defmodule Guides.Plugins.TypeOnlyOnEctoSchema do
1717
use TypedStructor.Plugin
1818

1919
@impl TypedStructor.Plugin
@@ -40,7 +40,7 @@ defmodule MyApp.User do
4040
use Ecto.Schema
4141

4242
typed_structor do
43-
plugin MyApp.TypedStructor.Plugins.TypeOnlyOnEctoSchema
43+
plugin Guides.Plugins.TypeOnlyOnEctoSchema
4444

4545
field :id, integer(), enforce: true
4646
field :name, String.t()
@@ -56,7 +56,7 @@ end
5656

5757
## Registering the plugin globally
5858
```elixir
59-
config :typed_structor, plugins: [MyApp.TypedStructor.Plugins.TypeOnlyOnEctoSchema]
59+
config :typed_structor, plugins: [Guides.Plugins.TypeOnlyOnEctoSchema]
6060
```
6161

6262
Note that the plugin is applied to **all modules** that use `TypedStructor`,
@@ -65,7 +65,7 @@ you can opt-out by determining the module name or other conditions.
6565
Let's change the plugin to only apply to modules from the `MyApp` namespace(*feel free to copy and paste*):
6666

6767
```elixir
68-
defmodule MyApp.TypedStructor.Plugins.TypeOnlyOnEctoSchema do
68+
defmodule Guides.Plugins.TypeOnlyOnEctoSchema do
6969
use TypedStructor.Plugin
7070

7171
@impl TypedStructor.Plugin
@@ -93,7 +93,7 @@ Now you can use `typed_structor` without registering the plugin explicitly:
9393
use Ecto.Schema
9494

9595
typed_structor do
96-
- plugin MyApp.TypedStructor.Plugins.TypeOnlyOnEctoSchema
96+
- plugin Guides.Plugins.TypeOnlyOnEctoSchema
9797

9898
field :id, integer(), enforce: true
9999
field :name, String.t()

mix.exs

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule TypedStructor.MixProject do
1111
elixir: "~> 1.14",
1212
elixirc_paths: elixirc_paths(Mix.env()),
1313
start_permanent: Mix.env() == :prod,
14+
consolidate_protocols: Mix.env() != :test,
1415
deps: deps(),
1516
name: "TypedStructor",
1617
source: @source_url,
@@ -61,6 +62,7 @@ defmodule TypedStructor.MixProject do
6162
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
6263
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
6364
{:ecto, "~> 3.0", only: [:dev, :test], optional: true},
65+
{:jason, "~> 1.4", only: [:dev, :test], optional: true},
6466
{:makeup_diff, "~> 0.1", only: [:test, :dev], runtime: false}
6567
]
6668
end
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
defmodule Guides.Plugins.EnumerableTest do
2+
use TypedStructor.GuideCase,
3+
async: true,
4+
guide: "derive_enumerable.md"
5+
6+
test "works" do
7+
user = %User{name: "Phil", age: 20}
8+
assert [:name, :age] === Enum.map(user, fn {key, _value} -> key end)
9+
assert [name: "Phil", age: 20] === Enum.to_list(user)
10+
end
11+
end

test/guides/plugins/jason_test.exs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
defmodule Guides.Plugins.JasonTest do
2+
use TypedStructor.GuideCase,
3+
async: true,
4+
guide: "derive_jason.md"
5+
6+
test "works" do
7+
assert {:ok, "{\"name\":\"Phil\"}"} === Jason.encode(%User{name: "Phil"})
8+
end
9+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
defmodule Guides.Plugins.PrimaryKeyAndTimestampsTest do
2+
use TypedStructor.GuideCase,
3+
async: true,
4+
guide: "primary_key_and_timestamps.md"
5+
6+
test "works", ctx do
7+
assert """
8+
@type t() :: %Guides.Plugins.PrimaryKeyAndTimestampsTest.User{
9+
__meta__: term(),
10+
age: integer(),
11+
id: integer(),
12+
inserted_at: NaiveDateTime.t(),
13+
name: String.t() | nil,
14+
updated_at: NaiveDateTime.t()
15+
}
16+
""" === types(ctx.registered.bytecode)
17+
18+
user = %User{name: "Phil"}
19+
assert 20 === user.age
20+
end
21+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
defmodule Guides.Plugins.TypeOnlyOnEctoSchema do
2+
use TypedStructor.GuideCase,
3+
async: true,
4+
guide: "type_only_on_ecto_schema.md"
5+
6+
test "works", ctx do
7+
assert """
8+
@type t() :: %Guides.Plugins.TypeOnlyOnEctoSchema.MyApp.User{
9+
__meta__: term(),
10+
age: integer(),
11+
id: integer(),
12+
name: String.t() | nil
13+
}
14+
""" === types(ctx.registered.bytecode)
15+
16+
user = %MyApp.User{name: "Phil"}
17+
assert 20 === user.age
18+
end
19+
end

test/support/guide_case.ex

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
defmodule TypedStructor.GuideCase do
2+
@moduledoc false
3+
4+
use ExUnit.CaseTemplate
5+
6+
using opts do
7+
guide = Keyword.fetch!(opts, :guide)
8+
9+
file = Path.expand([__DIR__, "../../../", "guides/plugins/", guide])
10+
11+
ast = Code.string_to_quoted!(extract_code(file))
12+
13+
quote do
14+
Code.compiler_options(debug_info: true)
15+
16+
{:module, _module_name, bytecode, _submodule} = unquote(ast)
17+
18+
ExUnit.Case.register_module_attribute(__MODULE__, :bytecode)
19+
@bytecode bytecode
20+
21+
import unquote(__MODULE__), only: [types: 1]
22+
end
23+
end
24+
25+
def types(bytecode) when is_binary(bytecode) do
26+
TypedStructor.TypeCase.types(bytecode) <> "\n"
27+
end
28+
29+
defp extract_code(file) do
30+
content =
31+
File.read!(file)
32+
33+
content
34+
|> String.split("\n")
35+
|> Enum.with_index(1)
36+
|> extract_code(%{in_code_block?: false, codes: []})
37+
|> case do
38+
%{codes: [implementation, usage | _resut]} ->
39+
"""
40+
#{implementation |> Enum.reverse() |> Enum.join("\n")}
41+
#{usage |> Enum.reverse() |> Enum.join("\n")}
42+
"""
43+
44+
ctx ->
45+
raise ArgumentError,
46+
"""
47+
Cannot find implementation and usage in file #{inspect(file)}
48+
context: #{inspect(ctx)}
49+
#{content}
50+
"""
51+
end
52+
end
53+
54+
@start "```elixir"
55+
@stop "```"
56+
57+
defp extract_code([], %{in_code_block?: false} = ctx) do
58+
%{ctx | codes: Enum.reverse(ctx.codes)}
59+
end
60+
61+
defp extract_code([{@start, _index} | rest], %{in_code_block?: false} = ctx) do
62+
extract_code(rest, %{ctx | in_code_block?: true, codes: [[] | ctx.codes]})
63+
end
64+
65+
defp extract_code([{@stop, _index} | rest], %{in_code_block?: true} = ctx) do
66+
extract_code(rest, %{ctx | in_code_block?: false})
67+
end
68+
69+
defp extract_code([{text, _index} | rest], %{in_code_block?: true} = ctx) do
70+
[current | codes] = ctx.codes
71+
extract_code(rest, %{ctx | codes: [[text | current] | codes]})
72+
end
73+
74+
defp extract_code([_text | rest], %{in_code_block?: false} = ctx) do
75+
extract_code(rest, ctx)
76+
end
77+
78+
defp extract_code([{text, index} | _rest], ctx) do
79+
raise ArgumentError,
80+
"""
81+
Unexpected text at line #{index}: #{inspect(text)}
82+
context: #{inspect(ctx)}
83+
"""
84+
end
85+
86+
defp extract_code([], ctx) do
87+
raise ArgumentError,
88+
"""
89+
Code block not closed
90+
context: #{inspect(ctx)}
91+
"""
92+
end
93+
end

0 commit comments

Comments
 (0)