Skip to content

Commit 3758ef5

Browse files
enh: params gen all label force
- [x] `params gen all` can decostruction destination, seems like `gen all` (parity feature). - [x] added `:label` in `params gen all` before decostruction, to hooks the param. It wins over variable name. - [x] refactoring. - [x] documentation. Signed-off-by: Gabriele Ghio <gabriele.ghio@secomind.com>
1 parent 6ec7730 commit 3758ef5

File tree

2 files changed

+213
-78
lines changed

2 files changed

+213
-78
lines changed

lib/astarte/utilities/params_gen.ex

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,18 +33,20 @@ defmodule Astarte.Generators.Utilities.ParamsGen do
3333
- **Seamless Integration:** Built on top of ExUnitProperties, it integrates smoothly with your property-based tests.
3434
- **Flexible Customization:** Accepts a keyword list for generator overrides, ensuring that only the generators you specify are replaced while the rest remain untouched.
3535
- **Improved Test Clarity:** By explicitly defining custom generators, tests become easier to understand and maintain.
36+
- **Labeled Clauses:** Bind overrides to a specific clause using labels to avoid conflicts and improve readability.
37+
- **Destructuring Parity:** Supports the same destructuring on the left-hand side as `gen all` (e.g., `%{k: v} = var <- ...`). The captured variable (e.g., `var`) acts as the hook for overrides when no label is used.
3638
3739
## Usage Examples
3840
3941
### Example
4042
41-
In this example, we override the default `a` integer generator with a tuple {2, 3, 4}.
43+
In this example, we override the default `a` integer generator with a generator that picks from [2, 3, 4].
4244
4345
defmodule MyGenerators do
4446
use Astarte.Generators.Utilities.ParamsGen
4547
4648
# Override the default integer generator using params gen all
47-
def parametric_generators(params \\ [a: { 2, 3, 4}])
49+
def parametric_generators(params \\ [a: member_of([2, 3, 4])])
4850
params gen all a <- integer(),
4951
b <- list_of(string(:ascii)),
5052
c <- constant({:atom, "string"}),
@@ -53,14 +55,53 @@ defmodule Astarte.Generators.Utilities.ParamsGen do
5355
end
5456
end
5557
58+
### Labeling a clause for overrides
59+
60+
Labels let you target a specific clause for overrides via `params:`. This is useful when the left side is a pattern (not a single variable), or when you want explicit names.
61+
62+
Supported form:
63+
64+
- Leading atom label (classic):
65+
66+
params gen all :payload, %{v: v} <- integer(), params: [payload: 42] do
67+
v
68+
end
69+
70+
With labels, the override uses the label name (e.g., `:payload`) instead of a variable name.
71+
72+
### Destructuring and variable hooks
73+
74+
`params gen all` fully supports destructuring on the left-hand side, mirroring `gen all`. When using a pattern match with an assignment, the variable on the right side of `=` becomes the hook name for the override (if no explicit label is provided).
75+
76+
params gen all %{b: b} = var <- string(?a..?a, length: 1),
77+
params: [var: %{b: 10}] do
78+
{b, var}
79+
end
80+
81+
In the example above, `var` is the hook name, so `params: [var: %{b: 10}]` overrides the generator for that clause; `b` will be `10` and `var` will be `%{b: 10}`.
82+
83+
### Label precedence over variable/destructuring
84+
85+
If you provide both a clause label and a variable (either a plain variable or a destructured one), the label wins. This ensures clear and explicit targeting of the clause, regardless of the variable name used in the pattern.
86+
87+
params gen all :payload,
88+
%{b: b} = captured <- string(?a..?a, length: 1),
89+
params: [payload: %{b: 10}] do
90+
{b, captured}
91+
end
92+
93+
In this case, the override uses the `:payload` label and not the `captured` variable name; `b` will be `10` and `captured` will be `%{b: 10}`.
94+
5695
## Notes
5796
5897
- **Integration with ExUnitProperties:** This macro leverages the existing functionality of ExUnitProperties,
5998
making it easy to adopt if you are already using property-based testing in your project.
6099
- **Macro Syntax:** The macro expects a keyword list under the `params:` key, where each key corresponds to
61-
a generator name (e.g., `a`, `b`) and each value is the custom generator or fixed params to be used.
100+
a generator name (e.g., `a`, `b`), the variable captured by a destructuring (e.g., `var` in `%{k: v} = var <- ...`),
101+
or a clause label (e.g., `:payload`). Each value is the custom generator or fixed params to be used.
62102
- **Fallback Behavior:** For generators not specified in the override list, the macro will default to using
63103
the original generator from ExUnitProperties.
104+
- **Precedence:** If both a label and a variable name are present for the same clause, the label takes precedence when resolving the override target.
64105
- **Compile-Time Verification:** Misuse or incorrect configuration will be flagged at compile time, helping
65106
you catch errors early in the development process.
66107
@@ -79,16 +120,37 @@ defmodule Astarte.Generators.Utilities.ParamsGen do
79120
end
80121
end
81122

82-
@doc false
83-
@spec params({:gen, any(), [{:all, any(), any()}]}) :: Macro.t()
84-
defmacro params({:gen, _gen_meta, [{:all, _all_meta, clauses_with_body}]}) do
85-
{clauses, [[do: body]]} = Enum.split(clauses_with_body, -1)
86-
compile(clauses, body)
87-
end
88-
89123
@doc """
90-
Defines a custom property-based test generator macro (`params gen all`) that supports overriding default generators.
91-
It processes generator clauses and applies any custom overrides provided via the `:params` keyword.
124+
Macro `params gen all` with targeted generator overrides.
125+
126+
Accepts a `params:` keyword list to override default generators or values, while keeping full
127+
syntax parity with `gen all` (including left-hand side destructuring).
128+
129+
- Hook: for each clause, the hook is the variable name on the left of `<-`.
130+
- Destructuring: with patterns like `%{k: v} = var <- ...`, the hook is `var`.
131+
- Label: you can label a clause by placing a leading atom (e.g., `:payload`).
132+
- Precedence: if both a variable (including destructured capture) and a `:label` are present, the label wins.
133+
134+
Examples
135+
136+
# Override by variable name
137+
params gen all a <- integer(),
138+
params: [a: constant(10)] do
139+
a
140+
end
141+
142+
# Destructuring: the captured variable acts as the hook
143+
params gen all %{b: b} = var <- string(?a..?a, length: 1),
144+
params: [var: %{b: 10}] do
145+
{b, var}
146+
end
147+
148+
# :label takes precedence over variable/destructuring
149+
params gen all :payload,
150+
%{b: b} = captured <- string(?a..?a, length: 1),
151+
params: [payload: %{b: 10}] do
152+
{b, captured}
153+
end
92154
"""
93155
@spec params({:gen, any(), [{:all, any(), list()}]}, [{:do, any()}]) :: Macro.t()
94156
defmacro params({:gen, _gen_meta, [{:all, _all_meta, clauses}]}, do: body) do
@@ -125,16 +187,37 @@ defmodule Astarte.Generators.Utilities.ParamsGen do
125187
]}
126188
end
127189

128-
defp compile_clauses([], _, acc), do: acc
129-
130-
defp compile_clauses([{:<-, _, [{param, _, _}, _]} = clause | tail], params, acc) do
190+
defp edit_clause([clause | tail], param, params, acc) do
131191
clause = override(clause, param, params)
132192
compile_clauses(tail, params, [clause | acc])
133193
end
134194

135-
defp compile_clauses([{:=, _, _} = clause | tail], params, acc) do
136-
compile_clauses(tail, params, [clause | acc])
137-
end
195+
defp compile_clauses([], _, acc), do: acc
196+
197+
defp compile_clauses(
198+
[label, {:<-, _, [{:=, _, [_, {_atom_param, _, _}]}, _]} = clause | tail],
199+
params,
200+
acc
201+
)
202+
when is_atom(label),
203+
do: edit_clause([clause | tail], label, params, acc)
204+
205+
defp compile_clauses([label, {:<-, _, [{_param, _, _}, _]} = clause | tail], params, acc)
206+
when is_atom(label),
207+
do: edit_clause([clause | tail], label, params, acc)
208+
209+
defp compile_clauses(
210+
[{:<-, _, [{:=, _, [_, {atom_param, _, _}]}, _]} = clause | tail],
211+
params,
212+
acc
213+
),
214+
do: edit_clause([clause | tail], atom_param, params, acc)
215+
216+
defp compile_clauses([{:<-, _, [{param, _, _}, _]} = clause | tail], params, acc),
217+
do: edit_clause([clause | tail], param, params, acc)
218+
219+
defp compile_clauses([{:=, _, _} = clause | tail], params, acc),
220+
do: compile_clauses(tail, params, [clause | acc])
138221

139222
defp split_clauses_and_params(clauses_and_params) do
140223
case Enum.split_while(clauses_and_params, &(not Keyword.keyword?(&1))) do

0 commit comments

Comments
 (0)