Hegel 0.3.5
Property-based testing for C++
Loading...
Searching...
No Matches
combinators.h
1#pragma once
2
3#include <variant>
4
5#include "hegel/core.h"
6#include "hegel/generators/numeric.h"
7#include "hegel/generators/primitives.h"
8
9namespace hegel::generators {
10
12 // Concrete IGenerator for sampled_from(). Schema asks the server for an
13 // integer index into the captured `elements_` vector; the client parser
14 // does the lookup.
15 template <typename T> class SampledFromGenerator : public IGenerator<T> {
16 public:
17 explicit SampledFromGenerator(std::vector<T> elements)
18 : elements_(std::move(elements)) {
19 if (elements_.empty()) {
20 throw std::invalid_argument(
21 "sampled_from requires a non-empty vector");
22 }
23 }
24
25 std::optional<BasicGenerator<T>> as_basic() const override {
26 hegel::internal::json::json schema = {
27 {"type", "integer"},
28 {"min_value", 0},
29 {"max_value", static_cast<int64_t>(elements_.size() - 1)}};
30 auto elements = elements_;
31 return BasicGenerator<T>{
32 std::move(schema),
33 [elements = std::move(elements)](
34 const hegel::internal::json::json_raw_ref& raw) {
35 return elements[static_cast<size_t>(raw.get_int64_t())];
36 }};
37 }
38
39 private:
40 std::vector<T> elements_;
41 };
42
43 // Concrete IGenerator for one_of(). Schema path requires every branch
44 // to be basic; fallback draws an index and delegates to that branch.
45 template <typename T> class OneOfGenerator : public IGenerator<T> {
46 public:
47 explicit OneOfGenerator(std::vector<Generator<T>> gens)
48 : gens_(std::move(gens)) {
49 if (gens_.empty()) {
50 throw std::invalid_argument(
51 "one_of requires a non-empty vector of generators");
52 }
53 }
54
55 std::optional<BasicGenerator<T>> as_basic() const override {
56 std::vector<BasicGenerator<T>> basics;
57 basics.reserve(gens_.size());
58 for (const auto& gen : gens_) {
59 auto b = gen.as_basic();
60 if (!b)
61 return std::nullopt;
62 basics.push_back(std::move(*b));
63 }
64
65 // Tag each branch with its index so the client knows which
66 // parser to apply. Schema per branch is [constant(i), value].
67 hegel::internal::json::json tagged =
68 hegel::internal::json::json::array();
69 for (size_t i = 0; i < basics.size(); ++i) {
70 hegel::internal::json::json elements =
71 hegel::internal::json::json::array();
72 elements.push_back(hegel::internal::json::json{
73 {"type", "constant"}, {"value", (int64_t)i}});
74 elements.push_back(basics[i].schema);
75 tagged.push_back(hegel::internal::json::json{
76 {"type", "tuple"}, {"elements", elements}});
77 }
78 hegel::internal::json::json schema = {{"type", "one_of"},
79 {"generators", tagged}};
80
81 return BasicGenerator<T>{
82 std::move(schema),
83 [basics = std::move(basics)](
84 const hegel::internal::json::json_raw_ref& raw) -> T {
85 size_t idx = static_cast<size_t>(raw[0].get_int64_t());
86 return basics[idx].parse_raw(raw[1]);
87 }};
88 }
89
90 T do_draw(const TestCase& tc) const override {
91 if (auto basic = as_basic()) {
92 return basic->do_draw(tc);
93 }
94 auto idx = integers<size_t>(
95 {.min_value = 0, .max_value = gens_.size() - 1})
96 .do_draw(tc);
97 return gens_[idx].do_draw(tc);
98 }
99
100 private:
101 std::vector<Generator<T>> gens_;
102 };
104
107
120 template <typename T>
121 Generator<T> sampled_from(const std::vector<T>& elements) {
122 return Generator<T>(new SampledFromGenerator<T>(elements));
123 }
124
131 template <typename T>
132 Generator<T> sampled_from(std::initializer_list<T> elements) {
133 return sampled_from(std::vector<T>(elements));
134 }
135
141 inline Generator<std::string>
142 sampled_from(std::initializer_list<const char*> elements) {
143 std::vector<std::string> strings;
144 strings.reserve(elements.size());
145 for (const char* s : elements) {
146 strings.push_back(s);
147 }
148 return sampled_from(strings);
149 }
150
152
155
172 template <typename T> Generator<T> one_of(std::vector<Generator<T>> gens) {
173 return Generator<T>(new OneOfGenerator<T>(std::move(gens)));
174 }
175
182 template <typename T>
183 Generator<T> one_of(std::initializer_list<Generator<T>> gens) {
184 return one_of(std::vector<Generator<T>>(gens));
185 }
186
188 namespace detail {
189
190 template <typename Variant, typename GenTuple, size_t I = 0>
191 Variant draw_variant_impl(const GenTuple& gens, size_t idx,
192 const TestCase& tc) {
193 if constexpr (I < std::tuple_size_v<GenTuple>) {
194 if (idx == I) {
195 return std::get<I>(gens).do_draw(tc);
196 }
197 return draw_variant_impl<Variant, GenTuple, I + 1>(gens, idx,
198 tc);
199 } else {
200 return Variant{};
201 }
202 }
203
204 template <typename Variant, typename Parsers, size_t I = 0>
205 Variant
206 parse_variant_impl(const Parsers& parsers, size_t idx,
207 const hegel::internal::json::json_raw_ref& raw) {
208 if constexpr (I < std::tuple_size_v<Parsers>) {
209 if (idx == I) {
210 return Variant{std::in_place_index<I>,
211 std::get<I>(parsers)(raw)};
212 }
213 return parse_variant_impl<Variant, Parsers, I + 1>(parsers, idx,
214 raw);
215 } else {
216 return Variant{};
217 }
218 }
219
220 } // namespace detail
221
222 // Concrete IGenerator for variant(). Schema path requires every branch
223 // to be basic and uses a tagged one_of (same wire format as
224 // OneOfGenerator, but branches can have heterogeneous types).
225 template <typename... Ts>
226 class VariantGenerator : public IGenerator<std::variant<Ts...>> {
227 public:
228 using Result = std::variant<Ts...>;
229
230 explicit VariantGenerator(Generator<Ts>... gens)
231 : gens_(std::move(gens)...) {}
232
233 std::optional<BasicGenerator<Result>> as_basic() const override {
234 auto basics = std::apply(
235 [](const auto&... g) {
236 return std::make_tuple(g.as_basic()...);
237 },
238 gens_);
239 bool all_basic = std::apply(
240 [](const auto&... b) { return (b.has_value() && ...); },
241 basics);
242 if (!all_basic)
243 return std::nullopt;
244
245 hegel::internal::json::json tagged =
246 hegel::internal::json::json::array();
247 size_t i = 0;
248 std::apply(
249 [&tagged, &i](const auto&... b) {
250 ((tagged.push_back(hegel::internal::json::json{
251 {"type", "tuple"},
252 {"elements", hegel::internal::json::json::array(
253 {hegel::internal::json::json{
254 {"type", "constant"},
255 {"value", (int64_t)i}},
256 b->schema})}}),
257 ++i),
258 ...);
259 },
260 basics);
261
262 hegel::internal::json::json schema = {{"type", "one_of"},
263 {"generators", tagged}};
264
265 auto parsers = std::apply(
266 [](const auto&... b) { return std::make_tuple(b->parse...); },
267 basics);
268
269 return BasicGenerator<Result>{
270 std::move(schema),
271 [parsers = std::move(parsers)](
272 const hegel::internal::json::json_raw_ref& raw) -> Result {
273 size_t idx = static_cast<size_t>(raw[0].get_int64_t());
274 return detail::parse_variant_impl<Result,
275 decltype(parsers)>(
276 parsers, idx, raw[1]);
277 }};
278 }
279
280 Result do_draw(const TestCase& tc) const override {
281 if (auto basic = as_basic()) {
282 return basic->do_draw(tc);
283 }
284 constexpr size_t N = sizeof...(Ts);
285 auto index_gen =
286 integers<size_t>({.min_value = 0, .max_value = N - 1});
287 size_t idx = index_gen.do_draw(tc);
288 return detail::draw_variant_impl<Result, decltype(gens_)>(gens_,
289 idx, tc);
290 }
291
292 private:
293 std::tuple<Generator<Ts>...> gens_;
294 };
295
296 // Concrete IGenerator for optional(). Schema path wraps the inner's
297 // schema in a one_of with a null branch; fallback uses booleans() to
298 // gate presence.
299 template <typename T>
300 class OptionalGenerator : public IGenerator<std::optional<T>> {
301 public:
302 explicit OptionalGenerator(Generator<T> gen) : gen_(std::move(gen)) {}
303
304 std::optional<BasicGenerator<std::optional<T>>>
305 as_basic() const override {
306 auto basic = gen_.as_basic();
307 if (!basic)
308 return std::nullopt;
309
310 hegel::internal::json::json generators =
311 hegel::internal::json::json::array();
312 generators.push_back(hegel::internal::json::json{{"type", "null"}});
313 generators.push_back(basic->schema);
314 hegel::internal::json::json schema = {{"type", "one_of"},
315 {"generators", generators}};
316
317 auto parse = basic->parse;
318 return BasicGenerator<std::optional<T>>{
319 std::move(schema),
320 [parse = std::move(parse)](
321 const hegel::internal::json::json_raw_ref& raw)
322 -> std::optional<T> {
323 if (raw.is_null()) {
324 return std::nullopt;
325 }
326 return parse(raw);
327 }};
328 }
329
330 std::optional<T> do_draw(const TestCase& tc) const override {
331 if (auto basic = as_basic()) {
332 return basic->do_draw(tc);
333 }
334 bool is_none = booleans().do_draw(tc);
335 if (is_none) {
336 return std::nullopt;
337 }
338 return gen_.do_draw(tc);
339 }
340
341 private:
342 Generator<T> gen_;
343 };
345
360 template <typename... Ts>
361 Generator<std::variant<Ts...>> variant(Generator<Ts>... gens) {
362 return Generator<std::variant<Ts...>>(
363 new VariantGenerator<Ts...>(std::move(gens)...));
364 }
365
381 template <typename T>
384 new OptionalGenerator<T>(std::move(gen)));
385 }
386
388
389} // namespace hegel::generators
Handle to the currently-executing test case.
Definition test_case.h:34
The base class of all generators.
Definition core.h:157
Hegel generators.
Definition core.h:16
Generator< T > sampled_from(const std::vector< T > &elements)
Sample from a fixed set of values.
Definition combinators.h:121
Generator< bool > booleans()
Generate random boolean values.
Generator< std::variant< Ts... > > variant(Generator< Ts >... gens)
Generate std::variant from heterogeneous generators.
Definition combinators.h:361
Generator< std::optional< T > > optional(Generator< T > gen)
Generate optional values (present or absent).
Definition combinators.h:382
Generator< T > one_of(std::vector< Generator< T > > gens)
Choose from multiple generators of the same type.
Definition combinators.h:172
*‍/
Definition core.h:94