Skip to content

Commit 4884c82

Browse files
author
Igor Barchenkov
authored
Add bang functions
1 parent 7dd8d02 commit 4884c82

8 files changed

Lines changed: 279 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## v0.2.0 (2021-04-24)
2+
3+
### Enhancements
4+
5+
* Add `build!/2`
6+
* Add `build_list!/3`
7+
* Add `insert!/3`
8+
* Add `insert_list!/4`
9+
10+
All the new functions raise `EctoStreamFactory.MissingKeyError` error if a generated struct, map or keyword list cannot be fully merged with the provided attributes.

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,12 @@ iex> build(:post, text: "Hello world")
126126
iex> build_list(2, :user, name: fn n -> "user#{n}" end)
127127
[%User{id: nil, name: "user1", age: 51, email: "n5@gmail.com"}, %User{id: nil, name: "user2", age: 40, email: "O6@yandex.com"}]
128128

129-
iex> insert(:user)
129+
iex> insert!(:user)
130130
%User{id: 1, name: "b", age: 23, email: "az7@gmail.com"}
131131

132+
iex> insert!(:user, gender: "female")
133+
** (EctoStreamFactory.MissingKeyError) MyApp.Factory.user_generator does not generate :gender field.
134+
132135
iex> insert(:user, [email: "az7@gmail.com"], on_conflict: :nothing)
133136
%User{id: nil, name: "c", age: 44, email: "az7@gmail.com"}
134137

lib/ecto_stream_factory/ecto_stream_factory.ex

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,48 @@ defmodule EctoStreamFactory do
3232

3333
@repo Factory.get_repo(__MODULE__, opts)
3434

35-
def build(generator_name, attrs \\ [])
36-
when is_atom(generator_name) or is_binary(generator_name) do
35+
defguardp is_valid_generator_name(value) when is_atom(value) or is_binary(value)
36+
defguardp is_valid_amount(value) when is_integer(value) and value > 0
37+
38+
def build(generator_name, attrs \\ []) when is_valid_generator_name(generator_name) do
3739
Factory.build(__MODULE__, generator_name, attrs)
3840
end
3941

42+
def build!(generator_name, attrs \\ []) when is_valid_generator_name(generator_name) do
43+
Factory.build!(__MODULE__, generator_name, attrs)
44+
end
45+
4046
def build_list(amount, generator_name, attrs \\ [])
41-
when (is_atom(generator_name) or is_binary(generator_name)) and
42-
(is_integer(amount) and amount > 0) do
47+
when is_valid_generator_name(generator_name) and
48+
is_valid_amount(amount) do
4349
Factory.build_list(__MODULE__, amount, generator_name, attrs)
4450
end
4551

52+
def build_list!(amount, generator_name, attrs \\ [])
53+
when is_valid_generator_name(generator_name) and
54+
is_valid_amount(amount) do
55+
Factory.build_list!(__MODULE__, amount, generator_name, attrs)
56+
end
57+
4658
def insert(generator_name, attrs \\ [], opts \\ [])
47-
when is_atom(generator_name) or is_binary(generator_name) do
59+
when is_valid_generator_name(generator_name) do
4860
Factory.insert(__MODULE__, @repo, generator_name, attrs, opts)
4961
end
5062

63+
def insert!(generator_name, attrs \\ [], opts \\ [])
64+
when is_valid_generator_name(generator_name) do
65+
Factory.insert!(__MODULE__, @repo, generator_name, attrs, opts)
66+
end
67+
5168
def insert_list(amount, generator_name, attrs \\ [], opts \\ [])
52-
when (is_atom(generator_name) or is_binary(generator_name)) and
53-
(is_integer(amount) and amount > 0) do
69+
when is_valid_generator_name(generator_name) and is_valid_amount(amount) do
5470
Factory.insert_list(__MODULE__, @repo, amount, generator_name, attrs, opts)
5571
end
72+
73+
def insert_list!(amount, generator_name, attrs \\ [], opts \\ [])
74+
when is_valid_generator_name(generator_name) and is_valid_amount(amount) do
75+
Factory.insert_list!(__MODULE__, @repo, amount, generator_name, attrs, opts)
76+
end
5677
end
5778
end
5879

@@ -73,9 +94,9 @@ defmodule EctoStreamFactory do
7394
@type insert_opts() :: Keyword.t()
7495

7596
@doc """
76-
Takes a signle instance from a StreamData [gen/1](`ExUnitProperties.gen/1`) generator.
97+
Takes a single instance from a StreamData [gen/1](`ExUnitProperties.gen/1`) generator.
7798
78-
If the generator returs a struct, map or keyword list, it also merges the result with the provided overwrites.
99+
If the generator returs a struct, map or keyword list, it also merges the result with `overwrites`.
79100
80101
## Examples
81102
@@ -87,6 +108,19 @@ defmodule EctoStreamFactory do
87108
"""
88109
@callback build(generator_name(), overwrites()) :: term()
89110

111+
@doc """
112+
Same as `c:build/2`, but raises an error if some key in `overwrites` does not exist in the generated struct, map or keyword list.
113+
114+
## Examples
115+
116+
iex> Factory.build!(:user)
117+
%User{id: nil, name: "b", age: 28}
118+
119+
iex> Factory.build!(:user, name: "foo", weight: 101)
120+
** (EctoStreamFactory.MissingKeyError) MyApp.Factory.user_generator does not generate :weight field.
121+
"""
122+
@callback build!(generator_name(), overwrites()) :: term()
123+
90124
@doc ~S"""
91125
Same as `c:build/2`, but instantiates a list of structs.
92126
@@ -97,6 +131,11 @@ defmodule EctoStreamFactory do
97131
"""
98132
@callback build_list(amount(), generator_name(), overwrites) :: nonempty_list(term())
99133

134+
@doc ~S"""
135+
Same as `c:build_list/3`, but raises an error if some key in `overwites` does not exist in the generated entities.
136+
"""
137+
@callback build_list!(amount(), generator_name(), overwrites) :: nonempty_list(term())
138+
100139
@doc """
101140
Similar to `c:build/2`, but expects a generator to return an Ecto.Schema struct to insert it into the database.
102141
@@ -107,6 +146,11 @@ defmodule EctoStreamFactory do
107146
"""
108147
@callback insert(generator_name(), overwrites(), insert_opts()) :: Schema.t()
109148

149+
@doc """
150+
Same as `c:insert/3`, but raises an error if some key in `overwites` does not exist in the generated struct.
151+
"""
152+
@callback insert!(generator_name(), overwrites(), insert_opts()) :: Schema.t()
153+
110154
@doc """
111155
Same as `c:insert/3`, but inserts a list of structs.
112156
@@ -117,4 +161,10 @@ defmodule EctoStreamFactory do
117161
"""
118162
@callback insert_list(amount(), generator_name(), overwrites(), insert_opts()) ::
119163
nonempty_list(Schema.t())
164+
165+
@doc ~S"""
166+
Same as `c:insert_list/4`, but raises an error if some key in `overwites` does not exist in the generated structs.
167+
"""
168+
@callback insert_list!(amount(), generator_name(), overwrites(), insert_opts()) ::
169+
nonempty_list(Schema.t())
120170
end
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
defmodule EctoStreamFactory.UndefinedGeneratorError do
2-
@moduledoc false
3-
42
defexception [:module, :generator_name]
53

64
@impl true
@@ -9,7 +7,7 @@ defmodule EctoStreamFactory.UndefinedGeneratorError do
97
No generator defined for #{inspect(gen_name)}.
108
Please check for typos or define your generator:
119
12-
defmodule #{module} do
10+
defmodule #{inspect(module)} do
1311
def #{EctoStreamFactory.Factory.function_from_generator_name(gen_name)} do
1412
...
1513
end
@@ -19,8 +17,6 @@ defmodule EctoStreamFactory.UndefinedGeneratorError do
1917
end
2018

2119
defmodule EctoStreamFactory.RepoNotSpecifiedError do
22-
@moduledoc false
23-
2420
defexception [:module]
2521

2622
@impl true
@@ -30,10 +26,21 @@ defmodule EctoStreamFactory.RepoNotSpecifiedError do
3026
"""
3127
:repo option is missing. Please configure the factory:
3228
33-
defmodule #{module} do
29+
defmodule #{inspect(module)} do
3430
use EctoStreamFactory, repo: #{repo_module}.Repo
3531
...
3632
end
3733
"""
3834
end
3935
end
36+
37+
defmodule EctoStreamFactory.MissingKeyError do
38+
defexception [:module, :generator_name, :key]
39+
40+
@impl true
41+
def message(%{module: module, generator_name: gen_name, key: key}) do
42+
"""
43+
#{inspect(module)}.#{EctoStreamFactory.Factory.function_from_generator_name(gen_name)} does not generate #{inspect(key)} field.
44+
"""
45+
end
46+
end

lib/ecto_stream_factory/factory.ex

Lines changed: 96 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,54 +2,140 @@ defmodule EctoStreamFactory.Factory do
22
@moduledoc false
33

44
def build(module, generator_name, attrs) do
5+
do_build(module, generator_name, attrs, &build_list/4)
6+
end
7+
8+
def build!(module, generator_name, attrs) do
9+
do_build(module, generator_name, attrs, &build_list!/4)
10+
end
11+
12+
defp do_build(module, generator_name, attrs, build_list_fun) do
513
module
6-
|> build_list(1, generator_name, attrs)
14+
|> build_list_fun.(1, generator_name, attrs)
715
|> hd()
816
end
917

1018
def build_list(module, count, generator_name, attrs) do
19+
do_build_list(module, count, generator_name, attrs, &create_records_stream/3)
20+
end
21+
22+
def build_list!(module, count, generator_name, attrs) do
23+
do_build_list(module, count, generator_name, attrs, &create_records_stream!/3)
24+
end
25+
26+
defp do_build_list(module, count, generator_name, attrs, create_records_stream_fun) do
1127
module
12-
|> create_records_stream(generator_name, attrs)
28+
|> create_records_stream_fun.(generator_name, attrs)
1329
|> Enum.take(count)
1430
end
1531

1632
def insert(module, repo, generator_name, attrs, opts) do
33+
do_insert(module, repo, generator_name, attrs, opts, &insert_list/6)
34+
end
35+
36+
def insert!(module, repo, generator_name, attrs, opts) do
37+
do_insert(module, repo, generator_name, attrs, opts, &insert_list!/6)
38+
end
39+
40+
def do_insert(module, repo, generator_name, attrs, opts, insert_list_fun) do
1741
module
18-
|> insert_list(repo, 1, generator_name, attrs, opts)
42+
|> insert_list_fun.(repo, 1, generator_name, attrs, opts)
1943
|> hd()
2044
end
2145

2246
def insert_list(module, repo, count, generator_name, attrs, opts) do
47+
do_insert_list(module, repo, count, generator_name, attrs, opts, &create_records_stream/3)
48+
end
49+
50+
def insert_list!(module, repo, count, generator_name, attrs, opts) do
51+
do_insert_list(module, repo, count, generator_name, attrs, opts, &create_records_stream!/3)
52+
end
53+
54+
def do_insert_list(module, repo, count, generator_name, attrs, opts, create_records_stream_fun) do
2355
module
24-
|> create_records_stream(generator_name, attrs)
56+
|> create_records_stream_fun.(generator_name, attrs)
2557
|> Stream.map(&insert_record(&1, repo, opts))
2658
|> Enum.take(count)
2759
end
2860

2961
defp create_records_stream(module, generator_name, attrs) do
62+
do_create_records_stream(module, generator_name, attrs, &merge_attributes/4)
63+
end
64+
65+
defp create_records_stream!(module, generator_name, attrs) do
66+
do_create_records_stream(module, generator_name, attrs, &merge_attributes!/4)
67+
end
68+
69+
defp do_create_records_stream(module, generator_name, attrs, merge_attrs_fun) do
3070
module
3171
|> create_stream(generator_name)
3272
|> Stream.with_index(1)
33-
|> Stream.map(&merge_attributes(&1, attrs))
73+
|> Stream.map(&merge_attrs_fun.(&1, attrs, module, generator_name))
3474
end
3575

36-
defp merge_attributes({record, index}, attrs) when is_struct(record) do
76+
defp merge_attributes({record, index}, attrs, _, _) when is_struct(record) do
3777
struct(record, prepare_attributes(attrs, index))
3878
end
3979

40-
defp merge_attributes({map, index}, attrs) when is_map(map) do
80+
defp merge_attributes({map, index}, attrs, _, _) when is_map(map) do
4181
attrs = attrs |> prepare_attributes(index) |> Map.new()
4282
Map.merge(map, attrs)
4383
end
4484

45-
defp merge_attributes({maybe_keyword, index}, attrs) do
85+
defp merge_attributes({maybe_keyword, index}, attrs, _, _) do
4686
if Keyword.keyword?(maybe_keyword) do
4787
Keyword.merge(maybe_keyword, prepare_attributes(attrs, index))
4888
else
4989
maybe_keyword
5090
end
5191
end
5292

93+
defp merge_attributes!({record, index}, attrs, module, generator_name) when is_struct(record) do
94+
try do
95+
struct!(record, prepare_attributes(attrs, index))
96+
rescue
97+
err in KeyError ->
98+
raise EctoStreamFactory.MissingKeyError,
99+
module: module,
100+
generator_name: generator_name,
101+
key: err.key
102+
end
103+
end
104+
105+
defp merge_attributes!({map, index}, attrs, module, generator_name) when is_map(map) do
106+
check_missing_key(map, attrs, &Map.has_key?/2, module, generator_name)
107+
108+
merge_attributes({map, index}, attrs, module, generator_name)
109+
end
110+
111+
defp merge_attributes!({maybe_keyword, index}, attrs, module, generator_name) do
112+
if Keyword.keyword?(maybe_keyword) do
113+
check_missing_key(maybe_keyword, attrs, &Keyword.has_key?/2, module, generator_name)
114+
end
115+
116+
merge_attributes({maybe_keyword, index}, attrs, module, generator_name)
117+
end
118+
119+
defp check_missing_key(map_or_keyword, attrs, has_key_fun, module, generator_name) do
120+
missing_key =
121+
attrs
122+
|> Keyword.keys()
123+
|> Enum.find_value(fn key ->
124+
if has_key_fun.(map_or_keyword, key) do
125+
false
126+
else
127+
key
128+
end
129+
end)
130+
131+
if missing_key do
132+
raise EctoStreamFactory.MissingKeyError,
133+
module: module,
134+
generator_name: generator_name,
135+
key: missing_key
136+
end
137+
end
138+
53139
defp prepare_attributes(attrs, index) do
54140
Enum.map(attrs, fn
55141
{key, fun} when is_function(fun, 1) ->
@@ -71,7 +157,7 @@ defmodule EctoStreamFactory.Factory do
71157
apply(module, function_name, [])
72158
else
73159
raise EctoStreamFactory.UndefinedGeneratorError,
74-
module: module_to_string(module),
160+
module: module,
75161
generator_name: generator_name
76162
end
77163
end
@@ -87,11 +173,7 @@ defmodule EctoStreamFactory.Factory do
87173
def get_repo(module, opts) do
88174
case Keyword.fetch(opts, :repo) do
89175
{:ok, repo} -> repo
90-
_ -> raise EctoStreamFactory.RepoNotSpecifiedError, module: module_to_string(module)
176+
_ -> raise EctoStreamFactory.RepoNotSpecifiedError, module: module
91177
end
92178
end
93-
94-
defp module_to_string(module) do
95-
module |> to_string() |> String.replace_prefix("Elixir.", "")
96-
end
97179
end

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule EctoStreamFactory.MixProject do
66
def project do
77
[
88
app: :ecto_stream_factory,
9-
version: "0.1.0",
9+
version: "0.2.0",
1010
elixir: ">= 1.10.0",
1111
start_permanent: Mix.env() == :prod,
1212
deps: deps(),

0 commit comments

Comments
 (0)