Entity Generator 4.2.0
Composable, deterministic entity generation for C++23
Loading...
Searching...
No Matches
entitygen.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2020-2026 Diego Dasso Migotto
3
4#ifndef DASMIG_ENTITYGEN_HPP
5#define DASMIG_ENTITYGEN_HPP
6
7#include "core.hpp"
8#include <algorithm>
9#include <any>
10#include <bit>
11#include <cmath>
12#include <concepts>
13#include <cstddef>
14#include <cstdint>
15#include <filesystem>
16#include <functional>
17#include <future>
18#include <iterator>
19#include <limits>
20#include <map>
21#include <memory>
22#include <optional>
23#include <ostream>
24#include <ranges>
25#include <set>
26#include <span>
27#include <stdexcept>
28#include <string>
29#include <string_view>
30#include <type_traits>
31#include <utility>
32#include <variant>
33#include <vector>
34
35/// @file entitygen.hpp
36/// @brief Entity generator library — composable, deterministic entity generation for C++23.
37/// @author Diego Dasso Migotto (diegomigotto at hotmail dot com)
38/// @see See doc/usage.md for the narrative tutorial.
39
40DASMIG_CORE_REQUIRE(1, 1);
41
42namespace dasmig
43{
44
45/// @brief A component value as JSON: null, a boolean, an integer, a number,
46/// a (UTF-8) string, an array or an object.
47///
48/// Objects keep their members in the order given. Integers convert
49/// implicitly to an integer value, floating-point numbers to a number
50/// (non-finite ones are written as null).
51/// @see component::to_json()
53{
54 public:
55 using array = std::vector<json_value>; ///< A JSON array.
56 using object = std::vector<std::pair<std::string, json_value>>; ///< A JSON object, members in order.
57
58 json_value() = default; ///< null.
59 json_value(std::nullptr_t) {} ///< null. // NOLINT(google-explicit-constructor)
60 json_value(std::string s) : _v{std::move(s)} {} ///< A string (UTF-8). // NOLINT(google-explicit-constructor)
61 json_value(const char* s) : _v{std::string{s}} {} ///< A string (UTF-8). // NOLINT(google-explicit-constructor)
62 json_value(array a) : _v{std::move(a)} {} ///< An array. // NOLINT(google-explicit-constructor)
63 json_value(object o) : _v{std::move(o)} {} ///< An object. // NOLINT(google-explicit-constructor)
64
65 /// @brief A boolean. Only `bool` itself: a pointer (such as a wide
66 /// literal `L"..."`) would otherwise convert and write `true`.
67 template <std::same_as<bool> B>
68 json_value(B b) : _v{b} {} // NOLINT(google-explicit-constructor)
69
70 /// @brief An integer.
71 template <std::integral I>
72 requires(!std::same_as<I, bool>)
73 json_value(I n) : _v{static_cast<std::int64_t>(n)} {} // NOLINT(google-explicit-constructor)
74
75 /// @brief A number; a non-finite one is null.
76 template <std::floating_point F>
77 json_value(F d) // NOLINT(google-explicit-constructor)
78 {
79 const auto x = static_cast<double>(d);
80 if (std::isfinite(x)) _v = x;
81 }
82
83 /// @brief The array held, or null when this is not an array.
84 [[nodiscard]] const array* get_array() const { return std::get_if<array>(&_v); }
85 /// @brief The object held, or null when this is not an object.
86 [[nodiscard]] const object* get_object() const { return std::get_if<object>(&_v); }
87 /// @brief The object held, or null when this is not an object.
88 [[nodiscard]] object* get_object() { return std::get_if<object>(&_v); }
89
90 /// @brief Write this value.
91 /// @param w Writer positioned where a value is allowed.
92 void write(core::json_writer& w) const
93 {
94 std::visit(
95 [&w](const auto& x) {
96 using X = std::decay_t<decltype(x)>;
97 if constexpr (std::is_same_v<X, array>)
98 {
99 w.begin_array();
100 for (const auto& item : x) item.write(w);
101 w.end_array();
102 }
103 else if constexpr (std::is_same_v<X, object>)
104 {
105 w.begin_object();
106 for (const auto& [name, item] : x)
107 {
108 w.key(name);
109 item.write(w);
110 }
111 w.end_object();
112 }
113 else
114 {
115 w.value(x);
116 }
117 },
118 _v);
119 }
120
121 private:
122 std::variant<std::nullptr_t, bool, std::int64_t, double, std::string, array, object> _v;
123};
124
125/// @brief A per-call input value: a boolean, an integer, a number or a string.
126///
127/// Scalars only, so the inputs can be part of the replay signature.
128/// @see entity_request::inputs, generation_context::input()
129using input_value = std::variant<bool, std::int64_t, double, std::string>;
130
131/// @brief Per-call inputs by name (ordered, so their hash is reproducible).
132using input_map = std::map<std::string, input_value, std::less<>>;
133
134// Forward declaration.
135class eg;
136
137/// @brief Context passed to components during generation.
138///
139/// Provides access to previously generated component values and a seeded
140/// random engine. Created and populated by the entity generator; not
141/// user-constructible.
142/// @see component::generate()
144{
145 public:
146 /// @brief Check if a component value has already been generated.
147 /// @param component_key The component key to look up.
148 /// @return `true` if a value for the key exists in the context.
149 [[nodiscard]] bool has(const std::wstring& component_key) const
150 {
151 return find_(component_key) != nullptr;
152 }
153
154 /// @brief Typed retrieval of an already-generated component value.
155 /// @tparam T The expected type of the stored value, or a const reference
156 /// to it (`get<const T&>`) to read it without a copy; the
157 /// reference is valid while the entity is being generated.
158 /// @param component_key The component key to retrieve.
159 /// @return The value cast to @p T.
160 /// @throws std::out_of_range If no value for the key was generated (the
161 /// message names the key); declare the dependency with
162 /// component::depends_on() so a request pulls it in.
163 /// @throws std::bad_any_cast If @p T does not match the stored type.
164 template <typename T>
165 [[nodiscard]] T get(const std::wstring& component_key) const
166 {
167 const auto* value = find_(component_key);
168 if (value == nullptr)
169 {
170 throw std::out_of_range("component '" + core::to_utf8(component_key) +
171 "' has not been generated");
172 }
173 return std::any_cast<T>(*value);
174 }
175
176 /// @brief Typed retrieval that tolerates a missing value.
177 /// @tparam T The expected type of the stored value.
178 /// @param component_key The component key to retrieve.
179 /// @return The value, or an empty optional if none was generated.
180 /// @throws std::bad_any_cast If @p T does not match the stored type.
181 template <typename T>
182 [[nodiscard]] std::optional<T> try_get(const std::wstring& component_key) const
183 {
184 const auto* value = find_(component_key);
185 if (value == nullptr) return std::nullopt;
186 return std::any_cast<T>(*value);
187 }
188
189 /// @brief Typed retrieval with a fallback.
190 /// @tparam T The expected type of the stored value.
191 /// @param component_key The component key to retrieve.
192 /// @param fallback Returned when no value was generated.
193 /// @return The value, or @p fallback.
194 /// @throws std::bad_any_cast If @p T does not match the stored type.
195 template <typename T>
196 [[nodiscard]] T get_or(const std::wstring& component_key, T fallback) const
197 {
198 const auto* value = find_(component_key);
199 return value == nullptr ? std::move(fallback) : std::any_cast<T>(*value);
200 }
201
202 /// @brief Check whether the request supplied an input.
203 /// @param name The input name.
204 /// @return `true` if entity_request::inputs has the name.
205 [[nodiscard]] bool has_input(std::string_view name) const
206 {
207 return _inputs != nullptr && _inputs->find(name) != _inputs->end();
208 }
209
210 /// @brief A per-call input from the request.
211 /// @tparam T One of `bool`, `std::int64_t`, `double`, `std::string`: the
212 /// type the input was given with.
213 /// @param name The input name.
214 /// @return The value.
215 /// @throws std::invalid_argument If the request has no such input, or it
216 /// has another type (the message names the input).
217 template <typename T>
218 [[nodiscard]] T input(std::string_view name) const
219 {
220 const auto it = _inputs == nullptr ? input_map::const_iterator{} : _inputs->find(name);
221 if (_inputs == nullptr || it == _inputs->end())
222 {
223 throw std::invalid_argument("input '" + std::string{name} + "' was not supplied");
224 }
225 const T* value = std::get_if<T>(&it->second);
226 if (value == nullptr)
227 {
228 throw std::invalid_argument("input '" + std::string{name} + "' has another type");
229 }
230 return *value;
231 }
232
233 /// @brief A per-call input, or a fallback when the request has none.
234 /// @tparam T The input type, as for input().
235 /// @param name The input name.
236 /// @param fallback Returned when the input was not supplied.
237 /// @return The value, or @p fallback.
238 /// @throws std::invalid_argument If the input has another type.
239 template <typename T>
240 [[nodiscard]] T input_or(std::string_view name, T fallback) const
241 {
242 return has_input(name) ? input<T>(name) : std::move(fallback);
243 }
244
245 /// @brief Access the random engine for this component.
246 ///
247 /// Seeded with the component's seed (entity::seed(key)), so the same
248 /// draws reproduce the value. Use it with the dasmig-core functions,
249 /// which give the same results on every platform:
250 /// @code
251 /// int age = dasmig::core::uniform_int(ctx.random(), 18, 65);
252 /// const auto& title = dasmig::core::pick(ctx.random(), titles);
253 /// bool lucky = dasmig::core::bernoulli(ctx.random(), 0.1);
254 /// @endcode
255 /// @return A reference to the per-component seeded engine.
256 [[nodiscard]] core::rng& random() const
257 {
258 return _random;
259 }
260
261 private:
262 // Private constructor, this is managed by the entity generator.
263 generation_context() = default;
264
265 // The value generated for a key, or nullptr.
266 [[nodiscard]] const std::any* find_(const std::wstring& component_key) const
267 {
268 const auto it = std::ranges::find(_values, component_key,
269 [](const auto& v) -> const std::wstring& { return *v.first; });
270 return it == _values.end() ? nullptr : it->second;
271 }
272
273 // Already-generated component values: the key and the value, both owned
274 // by the entity being generated (its entries never reallocate).
275 std::vector<std::pair<const std::wstring*, const std::any*>> _values;
276
277 // The request's inputs (owned by the request, which outlives the call).
278 const input_map* _inputs{nullptr};
279
280 // Per-component engine, reseeded before each component generates.
281 mutable core::rng _random{0};
282
283 // Allows entity generator to construct and populate contexts.
284 friend class eg;
285};
286
287/// @brief Abstract base for entity components.
288///
289/// Implement this interface to define custom components that the entity
290/// generator can produce. Each component has a unique key, generates typed
291/// random values, and converts them to display strings.
292/// @see generation_context, eg::add()
294{
295 public:
296 component() = default; ///< Default constructor.
297 component(const component&) = default; ///< Copyable.
298 component& operator=(const component&) = default; ///< Copyable.
299 component(component&&) noexcept = default; ///< Movable.
300 component& operator=(component&&) noexcept = default; ///< Movable.
301
302 /// @brief Virtual destructor for proper cleanup of derived classes.
303 virtual ~component() = default;
304
305 /// @brief Unique key identifying this component (e.g. `"name"`, `"age"`).
306 /// @return The component key as a wide string.
307 [[nodiscard]] virtual std::wstring key() const = 0;
308
309 /// @brief Generate a random value for this component.
310 /// @param ctx Context providing access to previously generated values
311 /// and a seeded random engine.
312 /// @return The generated value wrapped in `std::any`.
313 [[nodiscard]] virtual std::any generate(
314 const generation_context& ctx) const = 0;
315
316 /// @brief Convert a generated value to a displayable string.
317 /// @param value The value previously returned by generate().
318 /// @return A human-readable wide-string representation.
319 /// @see default_to_string()
320 [[nodiscard]] virtual std::wstring to_string(const std::any& value) const = 0;
321
322 /// @brief Convert a generated value to JSON, for entity::to_json().
323 ///
324 /// The default writes `bool` as a boolean, integers as integers,
325 /// `double` and `float` as numbers (non-finite ones as null),
326 /// `std::wstring` and `std::string` as strings, and anything else as the
327 /// string from to_string(). Override it to write a richer value: a
328 /// json_value can hold arrays and objects.
329 /// @param value The value previously returned by generate().
330 /// @return The JSON value.
331 [[nodiscard]] virtual json_value to_json(const std::any& value) const
332 {
333 return default_to_json(value, to_string(value));
334 }
335
336 /// @brief Display name of this component, used by entity::to_string().
337 ///
338 /// The key stays the identifier (lookups, seeds, JSON). A key with dots
339 /// is a path: `L"identity.age"` is written as `{"identity":{"age":..}}`.
340 /// @return The label; defaults to the key.
341 [[nodiscard]] virtual std::wstring label() const { return key(); }
342
343 /// @brief Keys of the components this one reads from the context.
344 ///
345 /// A request that selects this component also generates its dependencies
346 /// (transitively) and orders them before it, whatever the registration
347 /// order. If a dependency is skipped (weight roll or should_generate()),
348 /// this component is skipped too. A dependency that is not registered, or
349 /// a cycle, makes generation throw.
350 /// @return The dependency keys; empty by default.
351 [[nodiscard]] virtual std::vector<std::wstring> depends_on() const { return {}; }
352
353 /// @brief Hash of the data this component draws from, for replay.
354 ///
355 /// A component that calls a generator with data returns that generator's
356 /// `data_hash()`. entity::signature() combines the hashes of the components
357 /// that ran, so a replay shows whether the data changed.
358 /// @return The hash; 0 (no data) by default.
359 [[nodiscard]] virtual std::uint64_t data_hash() const { return 0; }
360
361 /// @brief Inclusion weight for this component (0.0 to 1.0).
362 ///
363 /// A value of 1.0 means always included; 0.5 means included roughly
364 /// half the time. The generator may override this per-component at
365 /// registration time via eg::add(comp, weight) or eg::weight().
366 /// @return The inclusion probability.
367 /// @see eg::weight()
368 [[nodiscard]] virtual double weight() const { return 1.0; }
369
370 /// @brief Validate a generated value.
371 /// @param value The value to validate.
372 /// @return `true` to accept, `false` to retry generation.
373 /// @see eg::max_retries()
374 [[nodiscard]] virtual bool validate([[maybe_unused]] const std::any& value) const
375 {
376 return true;
377 }
378
379 /// @brief Decide whether this component should be generated.
380 ///
381 /// Unlike weight() (probabilistic), this is logic-driven. Return
382 /// `false` to skip generation entirely based on the current context.
383 /// @param ctx Context with previously generated component values.
384 /// @return `true` to generate, `false` to skip.
385 [[nodiscard]] virtual bool should_generate(
386 [[maybe_unused]] const generation_context& ctx) const
387 {
388 return true;
389 }
390
391 protected:
392 /// @brief Default conversion covering common standard types.
393 ///
394 /// Handles `std::wstring`, `std::string` (UTF-8), `bool`, `float`,
395 /// `double` and the integer types `int`, `long`, `long long` and their
396 /// unsigned forms. Returns `"[?]"` for unrecognized types.
397 /// @param value The value to convert.
398 /// @return A wide-string representation.
399 [[nodiscard]] static std::wstring default_to_string(const std::any& value)
400 {
401 if (value.type() == typeid(std::wstring))
402 {
403 return std::any_cast<std::wstring>(value);
404 }
405 if (value.type() == typeid(int))
406 {
407 return std::to_wstring(std::any_cast<int>(value));
408 }
409 if (value.type() == typeid(double))
410 {
411 return std::to_wstring(std::any_cast<double>(value));
412 }
413 if (value.type() == typeid(float))
414 {
415 return std::to_wstring(std::any_cast<float>(value));
416 }
417 if (value.type() == typeid(long))
418 {
419 return std::to_wstring(std::any_cast<long>(value));
420 }
421 if (value.type() == typeid(bool))
422 {
423 return std::any_cast<bool>(value) ? L"true" : L"false";
424 }
425 if (value.type() == typeid(std::string))
426 {
427 return core::from_utf8(std::any_cast<const std::string&>(value));
428 }
429 if (value.type() == typeid(long long))
430 {
431 return std::to_wstring(std::any_cast<long long>(value));
432 }
433 if (value.type() == typeid(unsigned))
434 {
435 return std::to_wstring(std::any_cast<unsigned>(value));
436 }
437 if (value.type() == typeid(unsigned long))
438 {
439 return std::to_wstring(std::any_cast<unsigned long>(value));
440 }
441 if (value.type() == typeid(unsigned long long))
442 {
443 return std::to_wstring(std::any_cast<unsigned long long>(value));
444 }
445
446 return L"[?]";
447 }
448
449 /// @brief Default JSON conversion used by to_json().
450 /// @param value The value to convert.
451 /// @param display Its to_string() form, used for unrecognized types.
452 /// @return The JSON value.
453 [[nodiscard]] static json_value default_to_json(const std::any& value,
454 const std::wstring& display)
455 {
456 const auto& type = value.type();
457 if (type == typeid(bool)) return std::any_cast<bool>(value);
458 if (type == typeid(int)) return std::int64_t{std::any_cast<int>(value)};
459 if (type == typeid(long)) return std::int64_t{std::any_cast<long>(value)};
460 if (type == typeid(long long)) return std::int64_t{std::any_cast<long long>(value)};
461 if (type == typeid(unsigned)) return std::int64_t{std::any_cast<unsigned>(value)};
462 // Larger unsigned values stay exact as text rather than wrapping.
463 if (type == typeid(unsigned long) || type == typeid(unsigned long long))
464 {
465 const auto n = type == typeid(unsigned long)
466 ? static_cast<unsigned long long>(std::any_cast<unsigned long>(value))
467 : std::any_cast<unsigned long long>(value);
468 if (n <= static_cast<unsigned long long>(std::numeric_limits<std::int64_t>::max()))
469 return static_cast<std::int64_t>(n);
470 return std::to_string(n);
471 }
472 if (type == typeid(double) || type == typeid(float))
473 {
474 const double d = type == typeid(double) ? std::any_cast<double>(value)
475 : double{std::any_cast<float>(value)};
476 if (!std::isfinite(d)) return nullptr;
477 return d;
478 }
479 if (type == typeid(std::wstring)) return core::to_utf8(std::any_cast<std::wstring>(value));
480 if (type == typeid(std::string)) return std::any_cast<std::string>(value);
481 return core::to_utf8(display);
482 }
483};
484
485/// @name Generic Components
486/// @brief Reusable component templates for common patterns.
487/// @{
488
489/// @brief Tag type indicating that generic components should use default_to_string().
491
492/// @brief Component that always returns a fixed value.
493/// @tparam T The value type.
494/// @tparam Formatter Optional callable for custom string conversion
495/// (defaults to use_default_formatter).
496template <typename T, typename Formatter = use_default_formatter>
498{
499 public:
500 /// @brief Construct a constant component.
501 /// @param key Unique component key.
502 /// @param value The fixed value to always return.
503 /// @param fmt Optional custom formatter.
504 explicit constant_component(std::wstring key, T value,
505 Formatter fmt = {})
506 : _key{std::move(key)}, _value{std::move(value)},
507 _fmt{std::move(fmt)} {}
508
509 [[nodiscard]] std::wstring key() const override { return _key; }
510
511 [[nodiscard]] std::any generate(
512 const generation_context& /*ctx*/) const override
513 {
514 return _value;
515 }
516
517 [[nodiscard]] std::wstring to_string(const std::any& value) const override
518 {
519 if constexpr (std::is_same_v<Formatter, use_default_formatter>)
520 return default_to_string(value);
521 else
522 return _fmt(std::any_cast<T>(value));
523 }
524
525 private:
526 std::wstring _key;
527 T _value;
528 Formatter _fmt;
529};
530
531/// @brief Component that picks uniformly at random from a list of values.
532/// @tparam T The value type.
533/// @tparam Formatter Optional callable for custom string conversion.
534template <typename T, typename Formatter = use_default_formatter>
536{
537 public:
538 /// @brief Construct a choice component.
539 /// @param key Unique component key.
540 /// @param choices Non-empty list of values to choose from.
541 /// @param fmt Optional custom formatter.
542 /// @throws std::invalid_argument If @p choices is empty.
543 explicit choice_component(std::wstring key, std::vector<T> choices,
544 Formatter fmt = {})
545 : _key{std::move(key)}, _choices{std::move(choices)},
546 _fmt{std::move(fmt)}
547 {
548 if (_choices.empty())
549 throw std::invalid_argument("choices must not be empty");
550 }
551
552 [[nodiscard]] std::wstring key() const override { return _key; }
553
554 [[nodiscard]] std::any generate(
555 const generation_context& ctx) const override
556 {
557 return core::pick(ctx.random(), _choices);
558 }
559
560 [[nodiscard]] std::wstring to_string(const std::any& value) const override
561 {
562 if constexpr (std::is_same_v<Formatter, use_default_formatter>)
563 return default_to_string(value);
564 else
565 return _fmt(std::any_cast<T>(value));
566 }
567
568 private:
569 std::wstring _key;
570 std::vector<T> _choices;
571 Formatter _fmt;
572};
573
574/// @brief Component that generates a uniform random value: integers in
575/// [lo, hi], floating-point values in [lo, hi).
576/// @tparam T An integer type (not `bool`) or a floating-point type.
577/// @tparam Formatter Optional callable for custom string conversion.
578template <typename T, typename Formatter = use_default_formatter>
579 requires(std::integral<T> && !std::same_as<T, bool>) || std::floating_point<T>
581{
582 public:
583 /// @brief Construct a range component.
584 /// @param key Unique component key.
585 /// @param lo Lower bound (inclusive).
586 /// @param hi Upper bound (inclusive).
587 /// @param fmt Optional custom formatter.
588 explicit range_component(std::wstring key, T lo, T hi, // NOLINT(bugprone-easily-swappable-parameters)
589 Formatter fmt = {})
590 : _key{std::move(key)}, _lo{lo}, _hi{hi}, _fmt{std::move(fmt)} {}
591
592 [[nodiscard]] std::wstring key() const override { return _key; }
593
594 [[nodiscard]] std::any generate(
595 const generation_context& ctx) const override
596 {
597 if constexpr (std::floating_point<T>)
598 {
599 auto v = static_cast<T>(core::uniform_real(ctx.random(), static_cast<double>(_lo),
600 static_cast<double>(_hi)));
601 // Narrowing to float can round up to hi; keep the range half-open.
602 if (v >= _hi && _hi > _lo) v = std::nextafter(_hi, _lo);
603 return v;
604 }
605 else
606 {
607 return core::uniform_int(ctx.random(), _lo, _hi);
608 }
609 }
610
611 [[nodiscard]] std::wstring to_string(const std::any& value) const override
612 {
613 if constexpr (std::is_same_v<Formatter, use_default_formatter>)
614 return default_to_string(value);
615 else
616 return _fmt(std::any_cast<T>(value));
617 }
618
619 private:
620 std::wstring _key;
621 T _lo;
622 T _hi;
623 Formatter _fmt;
624};
625
626/// @brief Component that wraps a callable for one-off or computed values.
627///
628/// Avoids subclassing component entirely. The callable receives a
629/// generation_context and returns a value of type @p T.
630/// @tparam T The return type of the callable.
631/// @tparam GenFn Callable type taking `const generation_context&`.
632/// @tparam Formatter Optional callable for custom string conversion.
633template <typename T, typename GenFn, typename Formatter = use_default_formatter>
635{
636 public:
637 /// @brief Construct a callback component.
638 /// @param key Unique component key.
639 /// @param fn Callable invoked during generation.
640 /// @param fmt Optional custom formatter.
641 explicit callback_component(std::wstring key, GenFn fn,
642 Formatter fmt = {})
643 : _key{std::move(key)}, _fn{std::move(fn)}, _fmt{std::move(fmt)} {}
644
645 [[nodiscard]] std::wstring key() const override { return _key; }
646
647 [[nodiscard]] std::any generate(
648 const generation_context& ctx) const override
649 {
650 return _fn(ctx);
651 }
652
653 [[nodiscard]] std::wstring to_string(const std::any& value) const override
654 {
655 if constexpr (std::is_same_v<Formatter, use_default_formatter>)
656 return default_to_string(value);
657 else
658 return _fmt(std::any_cast<T>(value));
659 }
660
661 private:
662 std::wstring _key;
663 GenFn _fn;
664 Formatter _fmt;
665};
666
667/// @brief Deduction guide for callback_component.
668///
669/// Deduces @p T from the return type of @p GenFn so users don't need
670/// to spell out the lambda type explicitly.
671template <typename GenFn, typename Formatter = use_default_formatter>
672callback_component(std::wstring, GenFn, Formatter = {})
673 -> callback_component<
674 std::invoke_result_t<GenFn, const generation_context&>,
675 GenFn, Formatter>;
676
677/// @brief A component built from a callable, with every optional hook set by
678/// a chained call; see field().
679///
680/// The setters work on a temporary and return it, so a field is written in
681/// one expression and passed to eg::add():
682/// @code
683/// gen.add(dasmig::field(L"identity.age", [](const dasmig::generation_context& ctx) {
684/// return ctx.get<birth>(L"birth").age;
685/// })
686/// .label(L"Age")
687/// .depends_on({L"birth"}));
688/// @endcode
689/// @tparam T The value type the callable returns.
690template <typename T>
691class field_component final : public component
692{
693 public:
694 /// @brief The callable that produces the value.
695 using generator = std::function<T(const generation_context&)>;
696
697 /// @brief Construct a field.
698 /// @param key Unique component key (dots make a path in the JSON).
699 /// @param fn Callable invoked during generation.
700 field_component(std::wstring key, generator fn) : _key{std::move(key)}, _fn{std::move(fn)} {}
701
702 /// @brief Set the display label (default: the key).
703 [[nodiscard]] field_component label(std::wstring text) &&
704 {
705 _label = std::move(text);
706 return std::move(*this);
707 }
708
709 /// @brief Declare the components this one reads; see component::depends_on().
710 [[nodiscard]] field_component depends_on(std::vector<std::wstring> keys) &&
711 {
712 _depends = std::move(keys);
713 return std::move(*this);
714 }
715
716 /// @brief Set how the value is shown by to_string().
717 [[nodiscard]] field_component format(std::function<std::wstring(const T&)> fn) &&
718 {
719 _format = std::move(fn);
720 return std::move(*this);
721 }
722
723 /// @brief Set how the value is written by to_json().
724 [[nodiscard]] field_component json(std::function<json_value(const T&)> fn) &&
725 {
726 _json = std::move(fn);
727 return std::move(*this);
728 }
729
730 /// @brief Set the inclusion weight (0.0 to 1.0); see component::weight().
731 [[nodiscard]] field_component weight(double inclusion) &&
732 {
733 _weight = inclusion;
734 return std::move(*this);
735 }
736
737 /// @brief Generate the field only when the callable returns true;
738 /// see component::should_generate().
739 [[nodiscard]] field_component when(std::function<bool(const generation_context&)> fn) &&
740 {
741 _when = std::move(fn);
742 return std::move(*this);
743 }
744
745 /// @brief Accept only values the callable accepts; see component::validate().
746 [[nodiscard]] field_component validate(std::function<bool(const T&)> fn) &&
747 {
748 _validate = std::move(fn);
749 return std::move(*this);
750 }
751
752 /// @brief Set the hash of the data the field draws from; see component::data_hash().
753 [[nodiscard]] field_component data_hash(std::uint64_t hash) &&
754 {
755 _data_hash = hash;
756 return std::move(*this);
757 }
758
759 [[nodiscard]] std::wstring key() const override { return _key; }
760 [[nodiscard]] std::wstring label() const override { return _label.value_or(_key); }
761 [[nodiscard]] std::vector<std::wstring> depends_on() const override { return _depends; }
762 [[nodiscard]] std::uint64_t data_hash() const override { return _data_hash; }
763 [[nodiscard]] double weight() const override { return _weight; }
764
765 [[nodiscard]] std::any generate(const generation_context& ctx) const override
766 {
767 return std::any{_fn(ctx)};
768 }
769
770 [[nodiscard]] std::wstring to_string(const std::any& value) const override
771 {
772 return _format ? _format(std::any_cast<const T&>(value)) : default_to_string(value);
773 }
774
775 [[nodiscard]] json_value to_json(const std::any& value) const override
776 {
777 return _json ? _json(std::any_cast<const T&>(value))
778 : default_to_json(value, to_string(value));
779 }
780
781 [[nodiscard]] bool validate(const std::any& value) const override
782 {
783 return !_validate || _validate(std::any_cast<const T&>(value));
784 }
785
786 [[nodiscard]] bool should_generate(const generation_context& ctx) const override
787 {
788 return !_when || _when(ctx);
789 }
790
791 private:
792 std::wstring _key;
793 generator _fn;
794 std::optional<std::wstring> _label;
795 std::vector<std::wstring> _depends;
796 std::function<std::wstring(const T&)> _format;
797 std::function<json_value(const T&)> _json;
798 std::function<bool(const generation_context&)> _when;
799 std::function<bool(const T&)> _validate;
800 double _weight{1.0};
801 std::uint64_t _data_hash{0};
802};
803
804/// @brief Build a field from a callable; T is what the callable returns.
805/// @param key Unique component key.
806/// @param fn Callable taking `const generation_context&`.
807/// @return The field, to be configured with chained calls and added with eg::add().
808/// @see field_component
809template <typename Fn>
810[[nodiscard]] auto field(std::wstring key, Fn fn)
811{
813 std::move(key), std::move(fn)};
814}
815
816/// @brief Component that picks from a list of values using per-option weights.
817///
818/// Weights are relative (they don't need to sum to 1.0). A weight of 0
819/// means the option is never selected. The choice is dasmig-core's weighted
820/// choice (the same algorithm as core::weighted_table), built once.
821/// @tparam T The value type.
822/// @tparam Formatter Optional callable for custom string conversion.
823template <typename T, typename Formatter = use_default_formatter>
825{
826 public:
827 /// @brief A value–weight pair for weighted selection.
828 struct option
829 {
830 T value; ///< The selectable value.
831 double weight; ///< Relative selection weight (must be >= 0).
832 };
833
834 /// @brief Construct a weighted choice component.
835 /// @param key Unique component key.
836 /// @param options Non-empty list of value–weight pairs. At least one
837 /// must have a positive weight.
838 /// @param fmt Optional custom formatter.
839 /// @throws std::invalid_argument If @p options is empty, a weight is
840 /// negative or not finite, or all weights are zero.
841 explicit weighted_choice_component(std::wstring key,
842 std::vector<option> options,
843 Formatter fmt = {})
844 : _key{std::move(key)}, _options{std::move(options)},
845 _fmt{std::move(fmt)}, _dist{weights_of(_options)}
846 {
847 }
848
849 [[nodiscard]] std::wstring key() const override { return _key; }
850
851 [[nodiscard]] std::any generate(
852 const generation_context& ctx) const override
853 {
854 return _options[_dist(ctx.random())].value;
855 }
856
857 [[nodiscard]] std::wstring to_string(const std::any& value) const override
858 {
859 if constexpr (std::is_same_v<Formatter, use_default_formatter>)
860 return default_to_string(value);
861 else
862 return _fmt(std::any_cast<T>(value));
863 }
864
865 private:
866 // Checked here so the messages name the problem before core::discrete
867 // sees the weights.
868 static std::vector<double> weights_of(const std::vector<option>& options)
869 {
870 if (options.empty())
871 throw std::invalid_argument("options must not be empty");
872 if (!std::ranges::all_of(options, [](const auto& o) {
873 return std::isfinite(o.weight) && o.weight >= 0.0;
874 }))
875 throw std::invalid_argument("option weights must be finite and non-negative");
876 if (!std::ranges::any_of(options, [](const auto& o) { return o.weight > 0.0; }))
877 throw std::invalid_argument(
878 "at least one option must have a positive weight");
879 std::vector<double> weights;
880 weights.reserve(options.size());
881 std::ranges::transform(options, std::back_inserter(weights), &option::weight);
882 return weights;
883 }
884
885 std::wstring _key;
886 std::vector<option> _options;
887 Formatter _fmt;
888 core::discrete _dist;
889};
890
891/// @}
892
893/// @brief A generated entity holding component values in registration order.
894///
895/// Entities are produced by eg::generate and provide typed access to
896/// component values, seed signatures for replay, and serialization.
897/// @see eg::generate
899{
900 public:
901 /// @brief Typed retrieval of a component value by key.
902 /// @tparam T The expected type of the stored value.
903 /// @param component_key The component key to retrieve.
904 /// @return The value cast to @p T.
905 /// @throws std::out_of_range If the key is not present.
906 /// @throws std::bad_any_cast If @p T does not match the stored type.
907 template <typename T>
908 [[nodiscard]] T get(const std::wstring& component_key) const
909 {
910 return std::any_cast<T>(find_value(component_key));
911 }
912
913 /// @brief Type-erased retrieval of a component value by key.
914 /// @param component_key The component key to retrieve.
915 /// @return A const reference to the stored `std::any`.
916 /// @throws std::out_of_range If the key is not present.
917 [[nodiscard]] const std::any& get_any(const std::wstring& component_key) const
918 {
919 return find_value(component_key);
920 }
921
922 /// @brief Check if a component value exists by key.
923 /// @param component_key The component key to look up.
924 /// @return `true` if the entity contains a value for the key.
925 [[nodiscard]] bool has(const std::wstring& component_key) const
926 {
927 return std::ranges::any_of(_entries, [&component_key](const auto& e) {
928 return e.key == component_key;
929 });
930 }
931
932 /// @brief Retrieve the random seed used to generate a specific component.
933 /// @param component_key The component key.
934 /// @return The per-component seed for replay.
935 /// @throws std::out_of_range If the key is not present.
936 [[nodiscard]] std::uint64_t seed(const std::wstring& component_key) const
937 {
938 auto it = std::ranges::find(_entries, component_key, &entry::key);
939
940 if (it == _entries.end())
941 {
942 throw std::out_of_range("component not found");
943 }
944
945 return it->seed;
946 }
947
948 /// @brief Retrieve the random seed used to generate this entity.
949 ///
950 /// eg::generate(request, seed()) with the same request and setup reproduces
951 /// the entity:
952 /// it is the call seed, or the seed of the attempt the entity validator
953 /// accepted. Component @c k drew from
954 /// `core::rng{seed()}.split(core::to_utf8(k))`.
955 /// @return The entity-level seed.
956 [[nodiscard]] std::uint64_t seed() const
957 {
958 return _seed;
959 }
960
961 /// @brief The seed and the data this entity was generated from, in the
962 /// format every dasmig result uses.
963 /// @return The replay signature. `data` combines the request's inputs and
964 /// the component::data_hash() of every component that ran; it is
965 /// 0 when there are none, as for a generator without data.
966 [[nodiscard]] core::seed_signature signature() const
967 {
968 return {.seed = _seed, .data = _data};
969 }
970
971 /// @brief Write this entity as a JSON object
972 /// `{"components":{"key":value,…},"signature":{…}}`, components
973 /// in generation order, values from component::to_json().
974 ///
975 /// A key with dots is a path: `identity.name` and `identity.age` are
976 /// written as members of one `identity` object, placed where its first
977 /// member comes.
978 /// @param w Writer positioned where a value is allowed.
979 void to_json(core::json_writer& w) const
980 {
982 for (const auto& e : _entries) insert_path(root, core::to_utf8(e.key), e.json);
983 w.begin_object().key("components");
984 json_value{std::move(root)}.write(w);
985 w.key("signature");
986 signature().to_json(w);
987 w.end_object();
988 }
989
990 /// @brief This entity as a JSON document.
991 /// @return Compact JSON.
992 [[nodiscard]] std::string to_json() const
993 {
994 core::json_writer w;
995 to_json(w);
996 return w.str();
997 }
998
999 /// @brief Get all component keys present in this entity, in generation order.
1000 /// @return A vector of component keys.
1001 [[nodiscard]] std::vector<std::wstring> keys() const
1002 {
1003 std::vector<std::wstring> result;
1004 result.reserve(_entries.size());
1005 std::ranges::transform(_entries, std::back_inserter(result), &entry::key);
1006 return result;
1007 }
1008
1009 /// @brief Return component values as a map of key to display string.
1010 /// @return An ordered map of key → formatted value.
1011 [[nodiscard]] std::map<std::wstring, std::wstring> to_map() const
1012 {
1013 std::map<std::wstring, std::wstring> result;
1014 for (const auto& e : _entries)
1015 {
1016 result.emplace(e.key, e.display);
1017 }
1018 return result;
1019 }
1020
1021 /// @brief Number of component values in this entity.
1022 [[nodiscard]] std::size_t size() const { return _entries.size(); }
1023
1024 /// @brief Check if the entity has no component values.
1025 [[nodiscard]] bool empty() const { return _entries.empty(); }
1026
1027 /// @brief Convert all component values to a single formatted string.
1028 ///
1029 /// Each entry is rendered as `"label: display"` separated by two spaces.
1030 /// @return The formatted string, or empty if the entity is empty.
1031 [[nodiscard]] std::wstring to_string() const
1032 {
1033 // A plain loop: std::ranges::fold_left_first is missing from libc++
1034 // 18, Apple's libc++ and Emscripten.
1035 std::wstring result;
1036 for (const auto& e : _entries)
1037 {
1038 if (!result.empty()) result += L" ";
1039 result += e.label + L": " + e.display;
1040 }
1041 return result;
1042 }
1043
1044 /// @brief Stream all component values in generation order.
1045 friend std::wostream& operator<<(std::wostream& wos, const entity& e)
1046 {
1047 return wos << e.to_string();
1048 }
1049
1050 private:
1051 // A single component entry in generation order.
1052 struct entry
1053 {
1054 std::wstring key;
1055 std::wstring label;
1056 std::any value;
1057 std::wstring display;
1058 std::uint64_t seed;
1059 json_value json;
1060 };
1061
1062 // Private constructor, this is mostly a helper class to the entity
1063 // generator, not the intended API.
1064 entity() = default;
1065
1066 // Find a component value by key.
1067 [[nodiscard]] const std::any& find_value(const std::wstring& component_key) const
1068 {
1069 auto it = std::ranges::find(_entries, component_key, &entry::key);
1070
1071 if (it == _entries.end())
1072 {
1073 throw std::out_of_range("component not found");
1074 }
1075
1076 return it->value;
1077 }
1078
1079 // Add a value at a dotted path, creating the objects on the way.
1080 // Registration rejects keys that would collide, so a path is never both
1081 // a value and an object.
1082 static void insert_path(json_value::object& root, std::string_view path, const json_value& value)
1083 {
1084 json_value::object* level = &root;
1085 for (auto dot = path.find('.'); dot != std::string_view::npos; dot = path.find('.'))
1086 {
1087 const std::string_view name = path.substr(0, dot);
1088 path.remove_prefix(dot + 1);
1089 auto it = std::ranges::find(*level, name, &json_value::object::value_type::first);
1090 if (it == level->end())
1091 {
1092 level->emplace_back(std::string{name}, json_value::object{});
1093 it = std::prev(level->end());
1094 }
1095 level = it->second.get_object();
1096 if (level == nullptr) throw std::logic_error("component keys collide in the JSON");
1097 }
1098 level->emplace_back(std::string{path}, value);
1099 }
1100
1101 // Component entries in generation order.
1102 std::vector<entry> _entries;
1103
1104 // Seed used to generate this entity.
1105 std::uint64_t _seed{0};
1106
1107 // Hash of the inputs and of the data the components drew from (0: none).
1108 std::uint64_t _data{0};
1109
1110 // Allows entity generator to construct entities.
1111 friend class eg;
1112};
1113
1114/// @brief Observer interface for hooking into generation lifecycle events.
1115///
1116/// Override only the methods you care about; all default to no-ops.
1117/// Multiple observers can be attached to a generator via eg::add_observer().
1118/// @see eg::add_observer(), dasmig::ext::stats_observer
1120{
1121 public:
1122 generation_observer() = default; ///< Default constructor.
1123 generation_observer(const generation_observer&) = default; ///< Copyable.
1124 generation_observer& operator=(const generation_observer&) = default; ///< Copyable.
1125 generation_observer(generation_observer&&) noexcept = default; ///< Movable.
1126 generation_observer& operator=(generation_observer&&) noexcept = default; ///< Movable.
1127
1128 /// @brief Virtual destructor.
1129 virtual ~generation_observer() = default;
1130
1131 /// @name Entity Lifecycle
1132 /// @{
1133
1134 /// @brief Called before an entity is generated.
1135 virtual void on_before_generate() {}
1136 /// @brief Called after an entity is successfully generated.
1137 /// @param e The generated entity.
1138 virtual void on_after_generate([[maybe_unused]] const entity& e) {}
1139
1140 /// @}
1141 /// @name Component Generation
1142 /// @{
1143
1144 /// @brief Called before a component is generated.
1145 /// @param key The component key about to be generated.
1146 virtual void on_before_component([[maybe_unused]] const std::wstring& key) {}
1147 /// @brief Called after a component is successfully generated.
1148 /// @param key The component key.
1149 /// @param value The generated value.
1150 virtual void on_after_component([[maybe_unused]] const std::wstring& key,
1151 [[maybe_unused]] const std::any& value) {}
1152
1153 /// @}
1154 /// @name Component Skip
1155 /// @{
1156
1157 /// @brief Called when a component is skipped (weight roll or conditional exclusion).
1158 /// @param key The skipped component key.
1159 virtual void on_skip([[maybe_unused]] const std::wstring& key) {}
1160
1161 /// @}
1162 /// @name Component Validation
1163 /// @{
1164
1165 /// @brief Called before a component validation retry.
1166 /// @param key The component key being retried.
1167 /// @param attempt The retry attempt number (1-based).
1168 virtual void on_before_retry([[maybe_unused]] const std::wstring& key,
1169 [[maybe_unused]] std::size_t attempt) {}
1170 /// @brief Called after a component validation retry.
1171 /// @param key The component key.
1172 /// @param attempt The retry attempt number.
1173 /// @param value The newly generated value.
1174 virtual void on_after_retry([[maybe_unused]] const std::wstring& key,
1175 [[maybe_unused]] std::size_t attempt,
1176 [[maybe_unused]] const std::any& value) {}
1177
1178 /// @brief Called when component validation is exhausted (precedes exception).
1179 /// @param key The failed component key.
1180 virtual void on_component_fail([[maybe_unused]] const std::wstring& key) {}
1181
1182 /// @}
1183 /// @name Entity Validation
1184 /// @{
1185
1186 /// @brief Called before an entity validation retry.
1187 /// @param attempt The entity retry attempt number (1-based).
1188 virtual void on_before_entity_retry([[maybe_unused]] std::size_t attempt) {}
1189 /// @brief Called after an entity validation retry.
1190 /// @param attempt The entity retry attempt number.
1191 virtual void on_after_entity_retry([[maybe_unused]] std::size_t attempt) {}
1192
1193 /// @brief Called when entity validation is exhausted (precedes exception).
1194 virtual void on_entity_fail() {}
1195
1196 /// @}
1197 /// @name Component Registration
1198 /// @{
1199
1200 /// @brief Called before a component is registered.
1201 /// @param key The component key being added.
1202 virtual void on_before_add([[maybe_unused]] const std::wstring& key) {}
1203 /// @brief Called after a component is registered.
1204 /// @param key The component key that was added.
1205 virtual void on_after_add([[maybe_unused]] const std::wstring& key) {}
1206
1207 /// @brief Called before a component is removed.
1208 /// @param key The component key being removed.
1209 virtual void on_before_remove([[maybe_unused]] const std::wstring& key) {}
1210 /// @brief Called after a component is removed.
1211 /// @param key The component key that was removed.
1212 virtual void on_after_remove([[maybe_unused]] const std::wstring& key) {}
1213
1214 /// @}
1215};
1216
1217/// @brief Per-call entity generation and selection options.
1218///
1219/// Defaults select all registered components, generate one entity in a batch,
1220/// and accept entities without an entity-level validator. Component validation,
1221/// weights, conditions, registration order and the generator's retry policy
1222/// still apply. Replay requires the same request and instance setup.
1224{
1225 /// @brief Optional key subset; absent selects all, an empty vector selects none.
1226 /// Unknown keys are skipped and registration order is preserved.
1227 std::optional<std::vector<std::wstring>> component_keys{};
1228
1229 /// @brief Optional registered group name, mutually exclusive with component_keys.
1230 /// Required by eg::generate_group; also supported by the other generation methods.
1231 std::optional<std::wstring> group{};
1232
1233 /// @brief Number of entities for batch methods (ignored by single-entity methods).
1234 std::size_t count{1};
1235
1236 /// @brief Optional entity validator; false retries the whole entity.
1237 /// Concurrent batches require a thread-safe callable and captured state.
1238 std::function<bool(const entity&)> validator{};
1239
1240 /// @brief Per-call inputs for the components (a country, a reference
1241 /// date, a forced sex, ...), read with generation_context::input().
1242 /// They are part of the entity's replay signature. Give strings as
1243 /// `std::string{"BR"}`; a bare literal also works with C++23 compilers.
1245};
1246
1247/// @brief The entity generator — produces entities with configurable components.
1248///
1249/// Components are registered by implementing the component interface and
1250/// adding them to the generator. Components are generated in registration
1251/// order, allowing later components to access earlier ones via context.
1252/// @see component, entity, entity_request, generation_context
1253class eg
1254{
1255 public:
1256 /// @brief Default constructor creates an empty generator.
1257 eg() = default;
1258
1259 eg(const eg&) = delete; ///< Not copyable.
1260 eg& operator=(const eg&) = delete; ///< Not copyable.
1261 eg(eg&&) noexcept = default; ///< Move constructor.
1262 eg& operator=(eg&&) noexcept = default; ///< Move assignment.
1263 ~eg() = default; ///< Default destructor.
1264
1265 /// @brief Access the global singleton instance.
1266 ///
1267 /// For multi-threaded use, prefer creating independent eg instances.
1268 /// @return A reference to the singleton.
1269 static eg& instance()
1270 {
1271 static eg instance;
1272 return instance;
1273 }
1274
1275 /// @name Registration
1276 /// @{
1277
1278 /// @brief Register a component.
1279 ///
1280 /// Takes ownership of the component. If a component with the same key
1281 /// already exists, it will be replaced. Components are generated in the
1282 /// order they are registered.
1283 /// @param comp The component to register (moved).
1284 /// @return `*this` for chaining.
1285 /// @throws std::invalid_argument If the key has an empty path segment
1286 /// (`a..b`, `.a`, `a.`), or would be both a value and the parent of
1287 /// another key (`a` with `a.b`).
1288 eg& add(std::unique_ptr<component> comp)
1289 {
1290 const auto key = comp->key();
1291 check_key(key);
1293
1294 auto it = std::ranges::find(_components, key, &component_entry::key);
1295
1296 if (it != _components.end())
1297 {
1298 it->comp = std::move(comp);
1299 }
1300 else
1301 {
1302 _components.push_back({.key = key, .comp = std::move(comp)});
1303 }
1304
1306 return *this;
1307 }
1308
1309 /// @brief Register a field built with field().
1310 /// @tparam T The field's value type.
1311 /// @param f The field (moved).
1312 /// @return `*this` for chaining.
1313 /// @throws std::invalid_argument As add(std::unique_ptr<component>).
1314 template <typename T>
1316 {
1317 return add(std::make_unique<field_component<T>>(std::move(f)));
1318 }
1319
1320 /// @brief Register the components a recipe declares.
1321 ///
1322 /// A recipe is a tab-separated file with a header line and one component
1323 /// per row, so a component that only draws from data is written as data
1324 /// instead of code (see add_recipe_from_string() for the columns). The
1325 /// components are the ones the code makes with field(): the same
1326 /// dependencies, ordering, weights and replay. An invalid row throws and
1327 /// adds nothing.
1328 ///
1329 /// @param path The recipe file.
1330 /// @return `*this` for chaining.
1331 /// @throws core::tsv_error If the file cannot be read or a row is invalid.
1332 /// @throws std::invalid_argument As add(std::unique_ptr<component>).
1333 eg& add_recipe(const std::filesystem::path& path)
1334 {
1335 return add_recipe_(core::tsv_document::load(path));
1336 }
1337
1338 /// @brief Register the components a recipe in text declares.
1339 ///
1340 /// Columns (the header names them, in any order):
1341 ///
1342 /// | Column | | Meaning |
1343 /// |---|---|---|
1344 /// | `key` | required | The component key; dots make a path in the JSON |
1345 /// | `kind` | required | `constant`, `choice`, `weighted`, `int` or `real` |
1346 /// | `values` | required | What the kind draws from, below |
1347 /// | `label` | optional | Display name; the key by default |
1348 /// | `depends_on` | optional | Keys this component follows, separated by `;` |
1349 /// | `weight` | optional | Inclusion probability, 0 to 1; 1 by default |
1350 ///
1351 /// The kinds: `constant` gives `values` as text; `choice` picks uniformly
1352 /// from `a|b|c`; `weighted` picks from `a:3|b:1` (a value and its
1353 /// relative weight, split at the last colon); `int` draws an integer from
1354 /// `lo..hi` (both included); `real` draws a number from `lo..hi` (`hi`
1355 /// excluded). Text kinds give strings in the JSON, `int` and `real` give
1356 /// numbers. Items are not trimmed. A component with the key of an
1357 /// existing one replaces it. The recipe's rows are part of the entity's
1358 /// replay signature.
1359 ///
1360 /// @param text UTF-8 recipe text.
1361 /// @param source Name used in errors.
1362 /// @return `*this` for chaining.
1363 /// @throws core::tsv_error If the text is malformed or a row is invalid
1364 /// (an unknown kind, a bad range or weight, a repeated key).
1365 /// @throws std::invalid_argument As add(std::unique_ptr<component>).
1366 eg& add_recipe_from_string(std::string_view text, std::string source = "recipe")
1367 {
1368 return add_recipe_(core::tsv_document::parse(text, std::move(source)));
1369 }
1370
1371 /// @brief Register a component with a weight override.
1372 ///
1373 /// The override takes precedence over the component's own weight() method.
1374 /// @param comp The component to register (moved).
1375 /// @param weight_override Inclusion probability override (0.0–1.0).
1376 /// @return `*this` for chaining.
1377 eg& add(std::unique_ptr<component> comp, double weight_override)
1378 {
1379 const auto key = comp->key();
1380 add(std::move(comp));
1381 _weight_overrides[key] = weight_override;
1382 return *this;
1383 }
1384
1385 /// @brief Set or update the weight override for a registered component.
1386 /// @param component_key The component key.
1387 /// @param weight_value New inclusion probability (0.0–1.0).
1388 /// @return `*this` for chaining.
1389 /// @throws std::out_of_range If the component is not registered.
1390 eg& weight(const std::wstring& component_key, double weight_value)
1391 {
1392 if (!has(component_key))
1393 {
1394 throw std::out_of_range("component not found");
1395 }
1396
1397 _weight_overrides[component_key] = weight_value;
1398 return *this;
1399 }
1400
1401 /// @brief Remove a registered component by key.
1402 /// @param component_key The component key to remove.
1403 /// @return `*this` for chaining.
1404 eg& remove(const std::wstring& component_key)
1405 {
1406 notify(&generation_observer::on_before_remove, component_key);
1407
1408 std::erase_if(_components, [&component_key](const auto& entry) {
1409 return entry.key == component_key;
1410 });
1411 _weight_overrides.erase(component_key);
1412
1413 notify(&generation_observer::on_after_remove, component_key);
1414 return *this;
1415 }
1416
1417 /// @brief Check if a component is registered by key.
1418 /// @param component_key The component key to look up.
1419 /// @return `true` if a component with the key exists.
1420 [[nodiscard]] bool has(const std::wstring& component_key) const
1421 {
1422 return std::ranges::any_of(_components, [&component_key](const auto& entry) {
1423 return entry.key == component_key;
1424 });
1425 }
1426
1427 /// @brief Remove all registered components, weight overrides, and groups.
1428 /// @return `*this` for chaining.
1430 {
1431 _components.clear();
1432 _weight_overrides.clear();
1433 _groups.clear();
1434 return *this;
1435 }
1436
1437 /// @brief Return the number of registered components.
1438 [[nodiscard]] std::size_t size() const { return _components.size(); }
1439
1440 /// @brief Return all registered component keys in registration order.
1441 /// @return A vector of component keys.
1442 [[nodiscard]] std::vector<std::wstring> component_keys() const
1443 {
1444 std::vector<std::wstring> keys;
1445 keys.reserve(_components.size());
1446 std::ranges::transform(_components, std::back_inserter(keys),
1447 &component_entry::key);
1448 return keys;
1449 }
1450
1451 /// @}
1452 /// @name Validation
1453 /// @{
1454
1455 /// @brief Set the maximum number of retries for validation (default 10).
1456 ///
1457 /// Applies to both component-level and entity-level validation.
1458 /// @param retries The maximum retry count.
1459 /// @return `*this` for chaining.
1460 eg& max_retries(std::size_t retries)
1461 {
1462 _max_retries = retries;
1463 return *this;
1464 }
1465
1466 /// @}
1467 /// @name Observers
1468 /// @{
1469
1470 /// @brief Add an observer to receive generation lifecycle callbacks.
1471 /// @param obs The observer (shared ownership).
1472 /// @return `*this` for chaining.
1473 /// @see generation_observer
1474 eg& add_observer(std::shared_ptr<generation_observer> obs)
1475 {
1476 _observers.push_back(std::move(obs));
1477 return *this;
1478 }
1479
1480 /// @brief Remove a specific observer by identity.
1481 /// @param obs The observer to remove.
1482 /// @return `*this` for chaining.
1483 eg& remove_observer(const std::shared_ptr<generation_observer>& obs)
1484 {
1485 std::erase(_observers, obs);
1486 return *this;
1487 }
1488
1489 /// @brief Remove all observers.
1490 /// @return `*this` for chaining.
1492 {
1493 _observers.clear();
1494 return *this;
1495 }
1496
1497 /// @}
1498 /// @name Generation
1499 /// @{
1500
1501 /// @brief Generate an entity using per-call options.
1502 ///
1503 /// Draws a call seed from the generator's engine.
1504 /// @param request Selection and entity validation options.
1505 /// @return The generated entity.
1506 /// @throws std::runtime_error If component or entity validation is exhausted.
1507 /// @throws std::invalid_argument If both selection fields are set.
1508 /// @throws std::out_of_range If the requested group does not exist.
1509 [[nodiscard]] entity generate(const entity_request& request)
1510 {
1511 const auto refs = resolve_components(request);
1512 return generate_seeded(refs, _engine(), request);
1513 }
1514
1515 /// @brief Generate an entity using a request and a seed.
1516 ///
1517 /// The same seed and components give the same entity on every compiler
1518 /// and platform. Each component draws from a stream split from the seed
1519 /// by its key, so adding or removing a component does not change the
1520 /// others.
1521 /// @param request Selection and entity validation options.
1522 /// @param call_seed Seed for reproducible results; does not advance the internal engine.
1523 /// @return The generated entity.
1524 /// @throws std::runtime_error If validation is exhausted.
1525 /// @throws std::invalid_argument If both selection fields are set.
1526 /// @throws std::out_of_range If the requested group does not exist.
1527 [[nodiscard]] entity generate(const entity_request& request,
1528 std::uint64_t call_seed) const
1529 {
1530 return generate_seeded(resolve_components(request), call_seed, request);
1531 }
1532
1533 /// @}
1534 /// @name Batch Generation
1535 /// @{
1536
1537 /// @brief Generate request.count entities using per-call options.
1538 ///
1539 /// Draws one call seed from the generator's engine for the batch.
1540 /// @param request Selection, batch count and entity validation options.
1541 /// @return A vector of generated entities.
1542 /// @throws std::invalid_argument If both selection fields are set.
1543 /// @throws std::out_of_range If the requested group does not exist.
1544 /// @throws std::runtime_error If component or entity validation is exhausted.
1545 [[nodiscard]] std::vector<entity> generate_batch(const entity_request& request)
1546 {
1547 const auto refs = resolve_components(request);
1548 return generate_batch_seeded(refs, request, _engine());
1549 }
1550
1551 /// @brief Generate multiple entities using a seed.
1552 ///
1553 /// Entity @c i uses `core::item_seed(call_seed, i)` as its call seed, so
1554 /// the result equals generate_batch_async(request, call_seed) and each
1555 /// entity replays with generate(request, e.seed()). Since 4.1.0 this is
1556 /// the same seed derivation as every other library's batch; before, entity
1557 /// @c i used the @c i-th value of `core::rng{call_seed}.split("batch")`.
1558 /// @param request Selection, batch count and entity validation options.
1559 /// @param call_seed Seed for reproducible results.
1560 /// @return A vector of generated entities.
1561 /// @throws std::invalid_argument If both selection fields are set.
1562 /// @throws std::out_of_range If the requested group does not exist.
1563 /// @throws std::runtime_error If component or entity validation is exhausted.
1564 [[nodiscard]] std::vector<entity> generate_batch(
1565 const entity_request& request, std::uint64_t call_seed) const
1566 {
1567 return generate_batch_seeded(resolve_components(request), request, call_seed);
1568 }
1569
1570 /// @brief Generate a batch described by core::batch_options, from a seed
1571 /// drawn from the generator's engine.
1572 ///
1573 /// Uses `options.count` (the request's `count` is ignored here; it stays
1574 /// for generate_batch(request) until the next major version).
1575 /// @param request Selection and entity validation options.
1576 /// @param options Count, first index, threads and uniqueness.
1577 /// @return The entities, in index order.
1578 /// @throws std::invalid_argument If both selection fields are set.
1579 /// @throws std::out_of_range If the requested group does not exist.
1580 /// @throws std::runtime_error If component or entity validation is exhausted.
1581 [[nodiscard]] std::vector<entity> generate_batch(const entity_request& request,
1582 const core::batch_options& options)
1583 {
1584 return generate_batch(request, options, _engine());
1585 }
1586
1587 /// @brief Generate a batch described by core::batch_options, deterministically.
1588 ///
1589 /// Entity @c i is exactly `generate(request, core::item_seed(batch_seed,
1590 /// options.first + i))`, for any `options.threads`. With more than one
1591 /// thread, components, observers and the request's validator (including
1592 /// captured state) must be thread-safe.
1593 /// @param request Selection and entity validation options.
1594 /// @param options Count, first index, threads and uniqueness (see
1595 /// core::unique_rule, made for `entity`).
1596 /// @param batch_seed Seed of the whole batch.
1597 /// @return The entities, in index order.
1598 /// @throws std::invalid_argument If both selection fields are set.
1599 /// @throws std::out_of_range If the requested group does not exist.
1600 /// @throws std::runtime_error If component or entity validation is exhausted,
1601 /// or a unique batch runs out of attempts.
1602 [[nodiscard]] std::vector<entity> generate_batch(const entity_request& request,
1603 const core::batch_options& options,
1604 std::uint64_t batch_seed) const
1605 {
1606 const auto refs = resolve_components(request);
1607 return core::run_batch(options, batch_seed, [&](std::uint64_t item_seed)
1608 { return generate_seeded(refs, item_seed, request); });
1609 }
1610
1611 /// @brief Stream a batch of entities to a sink without holding the batch.
1612 ///
1613 /// Generates the same entities as generate_batch(request, options,
1614 /// batch_seed) and calls `sink(index, entity&)` in index order on the
1615 /// calling thread; `index` includes `options.first`.
1616 /// @tparam Sink Callable taking `(std::uint64_t, entity&)`; for example
1617 /// core::jsonl_writer.
1618 /// @param request Selection and entity validation options.
1619 /// @param options Count, first index, threads and uniqueness.
1620 /// @param batch_seed Seed of the whole batch.
1621 /// @param sink Receives the entities.
1622 template <typename Sink>
1623 void for_each_entity(const entity_request& request, const core::batch_options& options,
1624 std::uint64_t batch_seed, Sink&& sink) const
1625 {
1626 const auto refs = resolve_components(request);
1627 core::for_each_batch(options, batch_seed,
1628 [&](std::uint64_t item_seed) { return generate_seeded(refs, item_seed, request); },
1629 std::forward<Sink>(sink));
1630 }
1631
1632 /// @brief Stream a batch of entities with a batch seed drawn from the
1633 /// generator's engine.
1634 /// @tparam Sink Callable taking `(std::uint64_t, entity&)`.
1635 /// @param request Selection and entity validation options.
1636 /// @param options Count, first index, threads and uniqueness.
1637 /// @param sink Receives the entities.
1638 template <typename Sink>
1639 void for_each_entity(const entity_request& request, const core::batch_options& options,
1640 Sink&& sink)
1641 {
1642 for_each_entity(request, options, _engine(), std::forward<Sink>(sink));
1643 }
1644
1645 /// @}
1646 /// @name Concurrent Batch Generation
1647 /// @{
1648
1649 /// @brief Generate multiple entities concurrently.
1650 ///
1651 /// Same entities as generate_batch(request) would give from the same engine
1652 /// state; each entity is generated in its own async task. Results are
1653 /// returned in order. Components, observers and the request's validator
1654 /// (including captured state) must be thread-safe. In a WebAssembly build without threads
1655 /// (Emscripten without `-pthread`) the tasks run one after another, with
1656 /// the same results.
1657 /// @param request Selection, batch count and entity validation options.
1658 /// @return A vector of generated entities.
1659 /// @throws std::invalid_argument If both selection fields are set.
1660 /// @throws std::out_of_range If the requested group does not exist.
1661 /// @throws std::runtime_error If component or entity validation is exhausted.
1662 [[nodiscard]] std::vector<entity> generate_batch_async(const entity_request& request)
1663 {
1664 const auto refs = resolve_components(request);
1665 return generate_batch_async_seeded(refs, request, _engine());
1666 }
1667
1668 /// @brief Generate multiple entities concurrently using a seed.
1669 ///
1670 /// Gives the same entities as generate_batch(request, call_seed).
1671 /// @param request Selection, batch count and entity validation options.
1672 /// @param call_seed Seed for reproducible results.
1673 /// @return A vector of generated entities.
1674 /// @throws std::invalid_argument If both selection fields are set.
1675 /// @throws std::out_of_range If the requested group does not exist.
1676 /// @throws std::runtime_error If component or entity validation is exhausted.
1677 [[nodiscard]] std::vector<entity> generate_batch_async(
1678 const entity_request& request, std::uint64_t call_seed) const
1679 {
1680 return generate_batch_async_seeded(resolve_components(request), request, call_seed);
1681 }
1682
1683 /// @}
1684 /// @name Component Groups
1685 /// @{
1686
1687 /// @brief Register a named group of component keys.
1688 /// @param group_name Name for the group.
1689 /// @param component_keys Keys in the group.
1690 /// @return `*this` for chaining.
1691 eg& add_group(const std::wstring& group_name,
1692 std::vector<std::wstring> component_keys)
1693 {
1694 _groups[group_name] = std::move(component_keys);
1695 return *this;
1696 }
1697
1698 /// @brief Remove a named group.
1699 /// @param group_name The group to remove.
1700 /// @return `*this` for chaining.
1701 eg& remove_group(const std::wstring& group_name)
1702 {
1703 _groups.erase(group_name);
1704 return *this;
1705 }
1706
1707 /// @brief Check if a named group exists.
1708 /// @param group_name The group name to look up.
1709 /// @return `true` if the group exists.
1710 [[nodiscard]] bool has_group(const std::wstring& group_name) const
1711 {
1712 return _groups.contains(group_name);
1713 }
1714
1715 /// @brief Generate an entity using a named group.
1716 /// @param request Options with a registered group name.
1717 /// @return The generated entity.
1718 /// @throws std::out_of_range If the group does not exist.
1719 /// @throws std::invalid_argument If the group is absent or component_keys is set.
1720 /// @throws std::runtime_error If component or entity validation is exhausted.
1721 [[nodiscard]] entity generate_group(const entity_request& request)
1722 {
1723 require_group(request);
1724 return generate(request);
1725 }
1726
1727 /// @brief Generate an entity using a named group with a seed.
1728 /// @param request Options with a registered group name.
1729 /// @param call_seed Seed for reproducible results.
1730 /// @return The generated entity.
1731 /// @throws std::out_of_range If the group does not exist.
1732 /// @throws std::invalid_argument If the group is absent or component_keys is set.
1733 /// @throws std::runtime_error If component or entity validation is exhausted.
1734 [[nodiscard]] entity generate_group(const entity_request& request,
1735 std::uint64_t call_seed) const
1736 {
1737 require_group(request);
1738 return generate(request, call_seed);
1739 }
1740
1741 /// @}
1742
1743 private:
1744 // Component entry: key + owned component pointer.
1745 struct component_entry
1746 {
1747 std::wstring key;
1748 std::unique_ptr<component> comp;
1749 };
1750
1751 // Reference to a component entry for filtered generation.
1752 struct component_ref
1753 {
1754 std::wstring key;
1755 std::reference_wrapper<const component> comp;
1756 double effective_weight;
1757 std::vector<std::wstring> depends;
1758 };
1759
1760 // Type alias for the observer list.
1761 using observer_list =
1762 std::vector<std::shared_ptr<generation_observer>>;
1763
1764 // Notify all observers in a list by calling a member function. The
1765 // arguments go to every observer, so they are passed as lvalues (never
1766 // forwarded: the first observer could move from them).
1767 template <typename Hook, typename... Args>
1768 static void notify_all(const observer_list& observers,
1769 Hook hook, const Args&... args)
1770 {
1771 for (const auto& obs : observers)
1772 {
1773 ((*obs).*hook)(args...);
1774 }
1775 }
1776
1777 // Convenience: notify using the instance's observer list.
1778 template <typename Hook, typename... Args>
1779 void notify(Hook hook, const Args&... args) const
1780 {
1781 notify_all(_observers, hook, args...);
1782 }
1783
1784 // Emscripten without pthreads cannot start threads; deferred tasks give
1785 // the same entities, generated on the calling thread.
1786#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
1787 static constexpr std::launch async_policy = std::launch::deferred;
1788#else
1789 static constexpr std::launch async_policy = std::launch::async;
1790#endif
1791
1792 // The call seeds of a batch: item i is core::item_seed(call_seed, i).
1793 [[nodiscard]] static std::vector<std::uint64_t> batch_seeds(
1794 std::size_t count, std::uint64_t call_seed) // NOLINT(bugprone-easily-swappable-parameters)
1795 {
1796 std::vector<std::uint64_t> seeds(count);
1797 for (std::size_t i = 0; i < count; ++i)
1798 {
1799 seeds[i] = core::item_seed(call_seed, i);
1800 }
1801 return seeds;
1802 }
1803
1804 // One entity from a call seed: hooks, entity validation and retries.
1805 // Attempt 0 uses the call seed itself; attempt k uses the k-th value of
1806 // core::rng{call_seed}.split("attempt").
1807 [[nodiscard]] entity generate_seeded(std::span<const component_ref> refs,
1808 std::uint64_t call_seed,
1809 const entity_request& request) const
1810 {
1811 const auto& validator = request.validator;
1812 notify_all(_observers, &generation_observer::on_before_generate);
1813 auto attempts = core::rng{call_seed}.split("attempt");
1814 std::optional<entity> accepted;
1815 for (std::size_t attempt = 0; attempt <= _max_retries; ++attempt)
1816 {
1817 if (attempt > 0)
1818 notify_all(_observers,
1820 const std::uint64_t entity_seed = attempt == 0 ? call_seed : attempts();
1821 auto e = generate_impl(refs, entity_seed, _max_retries, _observers, request.inputs);
1822 if (attempt > 0)
1823 notify_all(_observers,
1825 if (!validator || validator(e))
1826 {
1827 accepted = std::move(e);
1828 break;
1829 }
1830 }
1831
1832 if (!accepted)
1833 {
1834 notify_all(_observers, &generation_observer::on_entity_fail);
1835 throw std::runtime_error(
1836 "entity validation failed after max retries");
1837 }
1838 notify_all(_observers, &generation_observer::on_after_generate, *accepted);
1839 return std::move(*accepted);
1840 }
1841
1842 // One generation attempt. Component k draws from
1843 // core::rng{entity_seed}.split(to_utf8(k)): its weight roll from
1844 // .split("weight"), its seeds (first try, then retries) from
1845 // .split("seed"); the component's own engine is core::rng{its seed}.
1846 [[nodiscard]] static entity generate_impl(
1847 std::span<const component_ref> components, std::uint64_t entity_seed, // NOLINT(bugprone-easily-swappable-parameters)
1848 std::size_t max_retries, const observer_list& observers, const input_map& inputs)
1849 {
1850 entity generated_entity;
1851 generation_context ctx;
1852 ctx._inputs = &inputs;
1853 generated_entity._seed = entity_seed;
1854 generated_entity._entries.reserve(components.size());
1855 ctx._values.reserve(components.size());
1856 const core::rng root{entity_seed};
1857
1858 for (const auto& ref : components)
1859 {
1860 const auto stream = root.split(core::to_utf8(ref.key));
1861
1862 // A component whose dependency did not run does not run either.
1863 if (!std::ranges::all_of(ref.depends, [&ctx](const auto& dep) { return ctx.has(dep); }))
1864 {
1865 notify_all(observers,
1867 continue;
1868 }
1869
1870 // Roll against the effective weight.
1871 if (ref.effective_weight < 1.0)
1872 {
1873 auto roll = stream.split("weight");
1874 if (core::uniform01(roll) >= ref.effective_weight)
1875 {
1876 notify_all(observers,
1878 continue;
1879 }
1880 }
1881
1882 // Conditional check: skip if the component opts out based on context.
1883 if (!ref.comp.get().should_generate(ctx))
1884 {
1885 notify_all(observers,
1887 continue;
1888 }
1889
1890 notify_all(observers,
1892
1893 auto seeds = stream.split("seed");
1894 auto component_seed = seeds();
1895 ctx._random = core::rng{component_seed};
1896 auto value = ref.comp.get().generate(ctx);
1897
1898 // Component-level validation with retries.
1899 for (std::size_t retry = 0;
1900 retry < max_retries && !ref.comp.get().validate(value);
1901 ++retry)
1902 {
1903 notify_all(observers,
1904 &generation_observer::on_before_retry, ref.key, retry + 1);
1905 component_seed = seeds();
1906 ctx._random = core::rng{component_seed};
1907 value = ref.comp.get().generate(ctx);
1908 notify_all(observers,
1910 retry + 1, value);
1911 }
1912
1913 if (!ref.comp.get().validate(value))
1914 {
1915 notify_all(observers,
1917 throw std::runtime_error(
1918 "component validation failed after max retries");
1919 }
1920
1921 auto display = ref.comp.get().to_string(value);
1922 auto json = ref.comp.get().to_json(value);
1923
1924 notify_all(observers,
1926
1927 const auto& added = generated_entity._entries.emplace_back(
1928 entity::entry{.key = ref.key, .label = ref.comp.get().label(),
1929 .value = std::move(value),
1930 .display = std::move(display),
1931 .seed = component_seed, .json = std::move(json)});
1932 ctx._values.emplace_back(&added.key, &added.value);
1933 }
1934
1935 generated_entity._data = replay_data(components, ctx, inputs);
1936 return generated_entity;
1937 }
1938
1939 // --- Component resolution ---------------------------------------------
1940
1941 static void require_group(const entity_request& request)
1942 {
1943 if (!request.group)
1944 throw std::invalid_argument("generate_group requires a group");
1945 }
1946
1947 [[nodiscard]] std::vector<component_ref> resolve_components(
1948 const entity_request& request) const
1949 {
1950 if (request.group && request.component_keys)
1951 throw std::invalid_argument("group and component_keys are mutually exclusive");
1952 if (request.group)
1953 {
1954 const auto it = _groups.find(*request.group);
1955 if (it == _groups.end())
1956 throw std::out_of_range("group not found");
1957 return with_dependencies(filter_components(it->second));
1958 }
1959 return with_dependencies(request.component_keys ? filter_components(*request.component_keys)
1960 : all_component_refs());
1961 }
1962
1963 [[nodiscard]] std::vector<entity> generate_batch_seeded(
1964 std::span<const component_ref> refs, const entity_request& request,
1965 std::uint64_t call_seed) const
1966 {
1967 std::vector<entity> entities;
1968 entities.reserve(request.count);
1969 for (const auto entity_seed : batch_seeds(request.count, call_seed))
1970 entities.push_back(generate_seeded(refs, entity_seed, request));
1971 return entities;
1972 }
1973
1974 [[nodiscard]] std::vector<entity> generate_batch_async_seeded(
1975 std::span<const component_ref> refs, const entity_request& request,
1976 std::uint64_t call_seed) const
1977 {
1978 std::vector<std::future<entity>> futures;
1979 futures.reserve(request.count);
1980 for (const auto entity_seed : batch_seeds(request.count, call_seed))
1981 {
1982 futures.push_back(std::async(async_policy, [this, refs, &request, entity_seed] {
1983 return generate_seeded(refs, entity_seed, request);
1984 }));
1985 }
1986
1987 std::vector<entity> entities;
1988 entities.reserve(request.count);
1989 std::ranges::transform(futures, std::back_inserter(entities),
1990 [](auto& f) { return f.get(); });
1991 return entities;
1992 }
1993
1994 // Build a component_ref span from the owned component entries.
1995 [[nodiscard]] std::vector<component_ref> all_component_refs() const
1996 {
1997 std::vector<component_ref> refs;
1998 refs.reserve(_components.size());
1999 std::ranges::transform(_components, std::back_inserter(refs),
2000 [this](const auto& entry) -> component_ref {
2001 return make_ref(entry);
2002 });
2003 return refs;
2004 }
2005
2006 // Filter registered components by the requested keys, preserving
2007 // registration order.
2008 [[nodiscard]] std::vector<component_ref> filter_components(
2009 std::span<const std::wstring> component_keys) const
2010 {
2011 std::vector<component_ref> filtered;
2012
2013 auto matching = _components
2014 | std::views::filter([&](const auto& entry) {
2015 return std::ranges::contains(component_keys, entry.key);
2016 })
2017 | std::views::transform([this](const auto& entry) -> component_ref {
2018 return make_ref(entry);
2019 });
2020 std::ranges::copy(matching, std::back_inserter(filtered));
2021
2022 return filtered;
2023 }
2024
2025 [[nodiscard]] component_ref make_ref(const component_entry& entry) const
2026 {
2027 return {entry.key, std::cref(*entry.comp), effective_weight(entry.key, *entry.comp),
2028 entry.comp->depends_on()};
2029 }
2030
2031 // Adds the dependencies of the selected components (transitively) and
2032 // orders the set so every component comes after its dependencies, keeping
2033 // registration order otherwise. A set without dependencies is returned
2034 // as it is.
2035 [[nodiscard]] std::vector<component_ref> with_dependencies(std::vector<component_ref> selected) const
2036 {
2037 if (std::ranges::all_of(selected, [](const auto& r) { return r.depends.empty(); }))
2038 return selected;
2039
2040 std::set<std::wstring> wanted;
2041 std::vector<std::wstring> pending;
2042 for (const auto& r : selected)
2043 {
2044 if (wanted.insert(r.key).second) pending.push_back(r.key);
2045 }
2046 for (std::size_t i = 0; i < pending.size(); ++i)
2047 {
2048 const auto it = std::ranges::find(_components, pending[i], &component_entry::key);
2049 for (const auto& dep : it->comp->depends_on())
2050 {
2051 if (!has(dep))
2052 throw std::invalid_argument("component '" + core::to_utf8(pending[i]) +
2053 "' depends on '" + core::to_utf8(dep) +
2054 "', which is not registered");
2055 if (wanted.insert(dep).second) pending.push_back(dep);
2056 }
2057 }
2058
2059 std::vector<component_ref> remaining;
2060 for (const auto& entry : _components)
2061 {
2062 if (wanted.contains(entry.key)) remaining.push_back(make_ref(entry));
2063 }
2064 std::vector<component_ref> ordered;
2065 std::set<std::wstring> emitted;
2066 while (!remaining.empty())
2067 {
2068 const auto next = std::ranges::find_if(remaining, [&emitted](const auto& r) {
2069 return std::ranges::all_of(r.depends, [&emitted](const auto& d) { return emitted.contains(d); });
2070 });
2071 if (next == remaining.end())
2072 {
2073 std::string names;
2074 for (const auto& r : remaining) names += (names.empty() ? "'" : ", '") + core::to_utf8(r.key) + "'";
2075 throw std::logic_error("components depend on each other in a cycle: " + names);
2076 }
2077 emitted.insert(next->key);
2078 ordered.push_back(std::move(*next));
2079 remaining.erase(next);
2080 }
2081 return ordered;
2082 }
2083
2084 // Hash of the request's inputs and of the data of the components that ran.
2085 // Nothing to hash gives 0.
2086 [[nodiscard]] static std::uint64_t replay_data(std::span<const component_ref> components,
2087 const generation_context& ctx,
2088 const input_map& inputs)
2089 {
2090 std::string bytes;
2091 const auto put_u64 = [&bytes](std::uint64_t n) {
2092 for (int shift = 0; shift < 64; shift += 8) bytes.push_back(static_cast<char>((n >> shift) & 0xFFU));
2093 };
2094 for (const auto& [name, value] : inputs)
2095 {
2096 bytes += 'i';
2097 put_u64(name.size());
2098 bytes += name;
2099 bytes += static_cast<char>(value.index());
2100 std::visit(
2101 [&](const auto& v) {
2102 using V = std::decay_t<decltype(v)>;
2103 if constexpr (std::is_same_v<V, bool>) bytes += v ? '\1' : '\0';
2104 else if constexpr (std::is_same_v<V, std::int64_t>) put_u64(static_cast<std::uint64_t>(v));
2105 else if constexpr (std::is_same_v<V, double>) put_u64(std::bit_cast<std::uint64_t>(v));
2106 else
2107 {
2108 put_u64(v.size());
2109 bytes += v;
2110 }
2111 },
2112 value);
2113 }
2114 for (const auto& ref : components)
2115 {
2116 const auto hash = ref.comp.get().data_hash();
2117 if (hash == 0 || !ctx.has(ref.key)) continue;
2118 const auto key = core::to_utf8(ref.key);
2119 bytes += 'c';
2120 put_u64(key.size());
2121 bytes += key;
2122 put_u64(hash);
2123 }
2124 return bytes.empty() ? 0 : core::fnv1a64(bytes);
2125 }
2126
2127 // Rejects keys the JSON could not hold: empty path segments, or a key
2128 // that is also the parent of another.
2129 // Build every component of a recipe, then add them, so a bad row adds none.
2130 eg& add_recipe_(const core::tsv_document& doc)
2131 {
2132 std::vector<std::unique_ptr<component>> built;
2133 std::set<std::string> seen;
2134 for (const auto& row : doc.rows())
2135 {
2136 const auto key = row.get<std::string>("key");
2137 if (!seen.insert(key).second) throw row.error("key", "key repeated in the recipe");
2138 built.push_back(recipe_component_(row, key));
2139 }
2140 for (const auto& comp : built) check_key(comp->key());
2141 for (auto& comp : built) add(std::move(comp));
2142 return *this;
2143 }
2144
2145 static std::unique_ptr<component> recipe_component_(const core::tsv_row& row, const std::string& key)
2146 {
2147 const auto kind = row.get<std::string>("kind");
2148 const auto values = row.get<std::string>("values");
2149 const auto hash = core::fnv1a64(key + '\t' + kind + '\t' + values);
2150
2151 if (kind == "constant")
2152 {
2153 return finish_recipe_(field(core::from_utf8(key),
2154 [text = core::from_utf8(values)](const generation_context&) { return text; }),
2155 row, key, hash);
2156 }
2157 if (kind == "choice")
2158 {
2159 std::vector<std::wstring> choices;
2160 for (const auto item : row.list("values", '|')) choices.push_back(core::from_utf8(item));
2161 return finish_recipe_(field(core::from_utf8(key),
2162 [choices](const generation_context& ctx)
2163 { return core::pick(ctx.random(), choices); }),
2164 row, key, hash);
2165 }
2166 if (kind == "weighted")
2167 {
2168 std::vector<std::wstring> choices;
2169 std::vector<double> weights;
2170 for (const auto item : row.list("values", '|'))
2171 {
2172 const auto colon = item.rfind(':');
2173 if (colon == std::string_view::npos)
2174 throw row.error("values", "expected value:weight, got '" + std::string{item} + "'");
2175 const auto weight = core::detail::parse_number<double>(item.substr(colon + 1));
2176 if (!weight || !(*weight >= 0.0))
2177 throw row.error("values", "weight must be a number of 0 or more in '" + std::string{item} + "'");
2178 choices.push_back(core::from_utf8(item.substr(0, colon)));
2179 weights.push_back(*weight);
2180 }
2181 if (!std::ranges::any_of(weights, [](double w) { return w > 0.0; }))
2182 throw row.error("values", "at least one weight must be positive");
2183 return finish_recipe_(field(core::from_utf8(key),
2184 [choices, dist = core::discrete{weights}](const generation_context& ctx)
2185 { return choices[dist(ctx.random())]; }),
2186 row, key, hash);
2187 }
2188 if (kind == "int" || kind == "real")
2189 {
2190 const std::string_view range = values;
2191 const auto dots = range.find("..");
2192 if (dots == std::string_view::npos)
2193 throw row.error("values", "expected lo..hi, got '" + values + "'");
2194 if (kind == "int")
2195 {
2196 const auto lo = core::detail::parse_number<std::int64_t>(range.substr(0, dots));
2197 const auto hi = core::detail::parse_number<std::int64_t>(range.substr(dots + 2));
2198 if (!lo || !hi || *lo > *hi)
2199 throw row.error("values", "expected whole numbers lo..hi with lo <= hi, got '" + values + "'");
2200 return finish_recipe_(field(core::from_utf8(key),
2201 [lo = *lo, hi = *hi](const generation_context& ctx)
2202 { return core::uniform_int(ctx.random(), lo, hi); }),
2203 row, key, hash);
2204 }
2205 const auto lo = core::detail::parse_number<double>(range.substr(0, dots));
2206 const auto hi = core::detail::parse_number<double>(range.substr(dots + 2));
2207 if (!lo || !hi || !(*lo <= *hi))
2208 throw row.error("values", "expected finite numbers lo..hi with lo <= hi, got '" + values + "'");
2209 return finish_recipe_(field(core::from_utf8(key),
2210 [lo = *lo, hi = *hi](const generation_context& ctx)
2211 {
2212 const double v = core::uniform_real(ctx.random(), lo, hi);
2213 return v >= hi && hi > lo ? std::nextafter(hi, lo) : v;
2214 }),
2215 row, key, hash);
2216 }
2217 throw row.error("kind", "expected constant, choice, weighted, int or real, got '" + kind + "'");
2218 }
2219
2220 // Apply the columns every kind shares and wrap the field as a component.
2221 template <typename T>
2222 static std::unique_ptr<component> finish_recipe_(field_component<T> f, const core::tsv_row& row,
2223 const std::string& key, std::uint64_t hash)
2224 {
2225 const auto label = row.get_or<std::string>("label", key);
2226 std::vector<std::wstring> depends;
2227 if (row.has("depends_on"))
2228 {
2229 for (const auto item : row.list("depends_on")) depends.push_back(core::from_utf8(item));
2230 }
2231 const auto weight = row.get_or<double>("weight", 1.0);
2232 if (!(weight >= 0.0 && weight <= 1.0)) throw row.error("weight", "must be between 0 and 1");
2233 return std::make_unique<field_component<T>>(std::move(f)
2234 .label(core::from_utf8(label))
2235 .depends_on(std::move(depends))
2236 .weight(weight)
2237 .data_hash(hash));
2238 }
2239
2240 void check_key(const std::wstring& key) const
2241 {
2242 const auto path = core::to_utf8(key);
2243 const auto bad = [&path](const std::string& why) {
2244 return std::invalid_argument("component key '" + path + "' " + why);
2245 };
2246 if (!path.empty() && (path.front() == '.' || path.back() == '.' || path.find("..") != std::string::npos))
2247 throw bad("has an empty path segment");
2248 for (const auto& other : _components)
2249 {
2250 if (other.key == key) continue;
2251 const auto o = core::to_utf8(other.key);
2252 if (o.starts_with(path + ".") || path.starts_with(o + "."))
2253 throw bad("collides with '" + o + "' (one is the parent of the other)");
2254 }
2255 }
2256
2257 // Resolve the effective weight for a component: override wins, then
2258 // the component's own weight() method.
2259 [[nodiscard]] double effective_weight(
2260 const std::wstring& key, const component& comp) const
2261 {
2262 auto it = _weight_overrides.find(key);
2263 return it != _weight_overrides.end() ? it->second : comp.weight();
2264 }
2265
2266 // Registered components in insertion order.
2267 std::vector<component_entry> _components;
2268
2269 // Named groups of component keys.
2270 std::map<std::wstring, std::vector<std::wstring>> _groups;
2271
2272 // Per-component weight overrides (key -> weight).
2273 std::map<std::wstring, double> _weight_overrides;
2274
2275 // Generation lifecycle observers (optional).
2276 observer_list _observers;
2277
2278 // Maximum retries for component and entity validation.
2279 std::size_t _max_retries{10};
2280
2281 // Internal random engine for the generator.
2282 core::rng _engine{core::rng::from_entropy()};
2283};
2284} // namespace dasmig
2285
2286#endif // DASMIG_ENTITYGEN_HPP
Component that wraps a callable for one-off or computed values.
std::any generate(const generation_context &ctx) const override
Generate a random value for this component.
callback_component(std::wstring key, GenFn fn, Formatter fmt={})
Construct a callback component.
std::wstring to_string(const std::any &value) const override
Convert a generated value to a displayable string.
std::wstring key() const override
Unique key identifying this component (e.g. "name", "age").
Component that picks uniformly at random from a list of values.
std::any generate(const generation_context &ctx) const override
Generate a random value for this component.
choice_component(std::wstring key, std::vector< T > choices, Formatter fmt={})
Construct a choice component.
std::wstring key() const override
Unique key identifying this component (e.g. "name", "age").
std::wstring to_string(const std::any &value) const override
Convert a generated value to a displayable string.
Abstract base for entity components.
component()=default
Default constructor.
virtual std::wstring label() const
Display name of this component, used by entity::to_string().
virtual std::uint64_t data_hash() const
Hash of the data this component draws from, for replay.
virtual std::any generate(const generation_context &ctx) const =0
Generate a random value for this component.
virtual std::wstring to_string(const std::any &value) const =0
Convert a generated value to a displayable string.
virtual bool validate(const std::any &value) const
Validate a generated value.
static std::wstring default_to_string(const std::any &value)
Default conversion covering common standard types.
virtual double weight() const
Inclusion weight for this component (0.0 to 1.0).
static json_value default_to_json(const std::any &value, const std::wstring &display)
Default JSON conversion used by to_json().
component(const component &)=default
Copyable.
virtual std::vector< std::wstring > depends_on() const
Keys of the components this one reads from the context.
virtual std::wstring key() const =0
Unique key identifying this component (e.g. "name", "age").
component & operator=(const component &)=default
Copyable.
virtual json_value to_json(const std::any &value) const
Convert a generated value to JSON, for entity::to_json().
component(component &&) noexcept=default
Movable.
virtual bool should_generate(const generation_context &ctx) const
Decide whether this component should be generated.
Component that always returns a fixed value.
std::wstring key() const override
Unique key identifying this component (e.g. "name", "age").
constant_component(std::wstring key, T value, Formatter fmt={})
Construct a constant component.
std::any generate(const generation_context &) const override
Generate a random value for this component.
std::wstring to_string(const std::any &value) const override
Convert a generated value to a displayable string.
The entity generator — produces entities with configurable components.
eg & max_retries(std::size_t retries)
Set the maximum number of retries for validation (default 10).
bool has(const std::wstring &component_key) const
Check if a component is registered by key.
std::vector< entity > generate_batch(const entity_request &request, const core::batch_options &options, std::uint64_t batch_seed) const
Generate a batch described by core::batch_options, deterministically.
std::vector< entity > generate_batch(const entity_request &request, std::uint64_t call_seed) const
Generate multiple entities using a seed.
std::vector< entity > generate_batch_async(const entity_request &request)
Generate multiple entities concurrently.
eg & clear_observers()
Remove all observers.
std::vector< entity > generate_batch_async(const entity_request &request, std::uint64_t call_seed) const
Generate multiple entities concurrently using a seed.
eg & weight(const std::wstring &component_key, double weight_value)
Set or update the weight override for a registered component.
entity generate(const entity_request &request, std::uint64_t call_seed) const
Generate an entity using a request and a seed.
entity generate_group(const entity_request &request)
Generate an entity using a named group.
eg & add_recipe_from_string(std::string_view text, std::string source="recipe")
Register the components a recipe in text declares.
eg & remove_observer(const std::shared_ptr< generation_observer > &obs)
Remove a specific observer by identity.
eg & remove_group(const std::wstring &group_name)
Remove a named group.
std::vector< entity > generate_batch(const entity_request &request, const core::batch_options &options)
Generate a batch described by core::batch_options, from a seed drawn from the generator's engine.
std::vector< std::wstring > component_keys() const
Return all registered component keys in registration order.
eg(eg &&) noexcept=default
Move constructor.
bool has_group(const std::wstring &group_name) const
Check if a named group exists.
eg & add(field_component< T > f)
Register a field built with field().
void for_each_entity(const entity_request &request, const core::batch_options &options, Sink &&sink)
Stream a batch of entities with a batch seed drawn from the generator's engine.
std::vector< entity > generate_batch(const entity_request &request)
Generate request.count entities using per-call options.
entity generate_group(const entity_request &request, std::uint64_t call_seed) const
Generate an entity using a named group with a seed.
eg(const eg &)=delete
Not copyable.
eg()=default
Default constructor creates an empty generator.
eg & add_recipe(const std::filesystem::path &path)
Register the components a recipe declares.
eg & add(std::unique_ptr< component > comp, double weight_override)
Register a component with a weight override.
eg & add_group(const std::wstring &group_name, std::vector< std::wstring > component_keys)
Register a named group of component keys.
void for_each_entity(const entity_request &request, const core::batch_options &options, std::uint64_t batch_seed, Sink &&sink) const
Stream a batch of entities to a sink without holding the batch.
static eg & instance()
Access the global singleton instance.
eg & remove(const std::wstring &component_key)
Remove a registered component by key.
eg & clear()
Remove all registered components, weight overrides, and groups.
std::size_t size() const
Return the number of registered components.
eg & add_observer(std::shared_ptr< generation_observer > obs)
Add an observer to receive generation lifecycle callbacks.
eg & operator=(const eg &)=delete
Not copyable.
entity generate(const entity_request &request)
Generate an entity using per-call options.
eg & add(std::unique_ptr< component > comp)
Register a component.
A generated entity holding component values in registration order.
const std::any & get_any(const std::wstring &component_key) const
Type-erased retrieval of a component value by key.
std::size_t size() const
Number of component values in this entity.
T get(const std::wstring &component_key) const
Typed retrieval of a component value by key.
std::uint64_t seed() const
Retrieve the random seed used to generate this entity.
friend std::wostream & operator<<(std::wostream &wos, const entity &e)
Stream all component values in generation order.
core::seed_signature signature() const
The seed and the data this entity was generated from, in the format every dasmig result uses.
std::uint64_t seed(const std::wstring &component_key) const
Retrieve the random seed used to generate a specific component.
std::string to_json() const
This entity as a JSON document.
bool has(const std::wstring &component_key) const
Check if a component value exists by key.
std::vector< std::wstring > keys() const
Get all component keys present in this entity, in generation order.
std::wstring to_string() const
Convert all component values to a single formatted string.
bool empty() const
Check if the entity has no component values.
void to_json(core::json_writer &w) const
Write this entity as a JSON object {"components":{"key":value,…},"signature":{…}},...
std::map< std::wstring, std::wstring > to_map() const
Return component values as a map of key to display string.
A component built from a callable, with every optional hook set by a chained call; see field().
field_component(std::wstring key, generator fn)
Construct a field.
field_component label(std::wstring text) &&
Set the display label (default: the key).
bool should_generate(const generation_context &ctx) const override
Decide whether this component should be generated.
field_component format(std::function< std::wstring(const T &)> fn) &&
Set how the value is shown by to_string().
field_component depends_on(std::vector< std::wstring > keys) &&
Declare the components this one reads; see component::depends_on().
std::function< T(const generation_context &)> generator
The callable that produces the value.
std::uint64_t data_hash() const override
Hash of the data this component draws from, for replay.
std::wstring to_string(const std::any &value) const override
Convert a generated value to a displayable string.
bool validate(const std::any &value) const override
Validate a generated value.
double weight() const override
Inclusion weight for this component (0.0 to 1.0).
field_component validate(std::function< bool(const T &)> fn) &&
Accept only values the callable accepts; see component::validate().
field_component weight(double inclusion) &&
Set the inclusion weight (0.0 to 1.0); see component::weight().
field_component data_hash(std::uint64_t hash) &&
Set the hash of the data the field draws from; see component::data_hash().
std::vector< std::wstring > depends_on() const override
Keys of the components this one reads from the context.
json_value to_json(const std::any &value) const override
Convert a generated value to JSON, for entity::to_json().
std::wstring label() const override
Display name of this component, used by entity::to_string().
std::any generate(const generation_context &ctx) const override
Generate a random value for this component.
std::wstring key() const override
Unique key identifying this component (e.g. "name", "age").
field_component when(std::function< bool(const generation_context &)> fn) &&
Generate the field only when the callable returns true; see component::should_generate().
field_component json(std::function< json_value(const T &)> fn) &&
Set how the value is written by to_json().
Context passed to components during generation.
bool has(const std::wstring &component_key) const
Check if a component value has already been generated.
T input_or(std::string_view name, T fallback) const
A per-call input, or a fallback when the request has none.
T get_or(const std::wstring &component_key, T fallback) const
Typed retrieval with a fallback.
T get(const std::wstring &component_key) const
Typed retrieval of an already-generated component value.
bool has_input(std::string_view name) const
Check whether the request supplied an input.
core::rng & random() const
Access the random engine for this component.
std::optional< T > try_get(const std::wstring &component_key) const
Typed retrieval that tolerates a missing value.
T input(std::string_view name) const
A per-call input from the request.
Observer interface for hooking into generation lifecycle events.
generation_observer(const generation_observer &)=default
Copyable.
generation_observer(generation_observer &&) noexcept=default
Movable.
virtual void on_after_entity_retry(std::size_t attempt)
Called after an entity validation retry.
virtual void on_after_retry(const std::wstring &key, std::size_t attempt, const std::any &value)
Called after a component validation retry.
virtual void on_before_remove(const std::wstring &key)
Called before a component is removed.
virtual void on_skip(const std::wstring &key)
Called when a component is skipped (weight roll or conditional exclusion).
virtual void on_after_remove(const std::wstring &key)
Called after a component is removed.
virtual void on_before_retry(const std::wstring &key, std::size_t attempt)
Called before a component validation retry.
virtual void on_before_component(const std::wstring &key)
Called before a component is generated.
virtual void on_after_component(const std::wstring &key, const std::any &value)
Called after a component is successfully generated.
virtual void on_after_add(const std::wstring &key)
Called after a component is registered.
virtual void on_before_add(const std::wstring &key)
Called before a component is registered.
generation_observer()=default
Default constructor.
virtual void on_before_entity_retry(std::size_t attempt)
Called before an entity validation retry.
generation_observer & operator=(const generation_observer &)=default
Copyable.
virtual void on_component_fail(const std::wstring &key)
Called when component validation is exhausted (precedes exception).
virtual void on_before_generate()
Called before an entity is generated.
virtual void on_entity_fail()
Called when entity validation is exhausted (precedes exception).
virtual void on_after_generate(const entity &e)
Called after an entity is successfully generated.
A component value as JSON: null, a boolean, an integer, a number, a (UTF-8) string,...
Definition entitygen.hpp:53
object * get_object()
The object held, or null when this is not an object.
Definition entitygen.hpp:88
std::vector< json_value > array
A JSON array.
Definition entitygen.hpp:55
void write(core::json_writer &w) const
Write this value.
Definition entitygen.hpp:92
const object * get_object() const
The object held, or null when this is not an object.
Definition entitygen.hpp:86
json_value(B b)
A boolean. Only bool itself: a pointer (such as a wide literal L"...") would otherwise convert and wr...
Definition entitygen.hpp:68
std::vector< std::pair< std::string, json_value > > object
A JSON object, members in order.
Definition entitygen.hpp:56
json_value(std::nullptr_t)
null. // NOLINT(google-explicit-constructor)
Definition entitygen.hpp:59
json_value(F d)
A number; a non-finite one is null.
Definition entitygen.hpp:77
const array * get_array() const
The array held, or null when this is not an array.
Definition entitygen.hpp:84
json_value(std::string s)
A string (UTF-8). // NOLINT(google-explicit-constructor)
Definition entitygen.hpp:60
json_value(const char *s)
A string (UTF-8). // NOLINT(google-explicit-constructor)
Definition entitygen.hpp:61
json_value()=default
null.
json_value(I n)
An integer.
Definition entitygen.hpp:73
json_value(object o)
An object. // NOLINT(google-explicit-constructor)
Definition entitygen.hpp:63
json_value(array a)
An array. // NOLINT(google-explicit-constructor)
Definition entitygen.hpp:62
Component that generates a uniform random value: integers in [lo, hi], floating-point values in [lo,...
range_component(std::wstring key, T lo, T hi, Formatter fmt={})
Construct a range component.
std::wstring to_string(const std::any &value) const override
Convert a generated value to a displayable string.
std::wstring key() const override
Unique key identifying this component (e.g. "name", "age").
std::any generate(const generation_context &ctx) const override
Generate a random value for this component.
Component that picks from a list of values using per-option weights.
std::wstring to_string(const std::any &value) const override
Convert a generated value to a displayable string.
std::any generate(const generation_context &ctx) const override
Generate a random value for this component.
weighted_choice_component(std::wstring key, std::vector< option > options, Formatter fmt={})
Construct a weighted choice component.
std::wstring key() const override
Unique key identifying this component (e.g. "name", "age").
auto field(std::wstring key, Fn fn)
Build a field from a callable; T is what the callable returns.
std::map< std::string, input_value, std::less<> > input_map
Per-call inputs by name (ordered, so their hash is reproducible).
std::variant< bool, std::int64_t, double, std::string > input_value
A per-call input value: a boolean, an integer, a number or a string.
Per-call entity generation and selection options.
input_map inputs
Per-call inputs for the components (a country, a reference date, a forced sex, ......
std::function< bool(const entity &)> validator
Optional entity validator; false retries the whole entity. Concurrent batches require a thread-safe c...
std::optional< std::vector< std::wstring > > component_keys
Optional key subset; absent selects all, an empty vector selects none. Unknown keys are skipped and r...
std::size_t count
Number of entities for batch methods (ignored by single-entity methods).
std::optional< std::wstring > group
Optional registered group name, mutually exclusive with component_keys. Required by eg::generate_grou...
Tag type indicating that generic components should use default_to_string().
A value–weight pair for weighted selection.
double weight
Relative selection weight (must be >= 0).