Skip to content

Commit b1d67cd

Browse files
committed
refactor: code quality overhaul, new APIs, tests, and docs
Code quality (entitygen.hpp): - Replace component_entry std::pair with named struct (.key, .comp) - Replace entity::to_string() loop with std::ranges::fold_left_first - Add default_to_string support for long type - Validate choice_component/weighted_choice_component constructors (throw std::invalid_argument on empty or all-zero weights) New APIs: - entity::size(), entity::empty() - eg::clear(), eg::size(), eg::component_keys() - operator<<(wostream, stats_observer) Rename eg::count() → eg::size() for STL consistency with entity::size(); count() in C++ idiom means "occurrences of X", size() means "total number of elements". Tests (254 cases, 746 assertions): - ~35 new test cases: input validation, boundary conditions, error messages, edge cases, move semantics, observer ordering, async failure, stream operator, empty entity serialization - Fix shallow stats assertions with deterministic retry counts - Simplify clear_generator fixture to use eg::clear() Docs: - Document default_to_string supported types - Document entity::get_any(), keys(), size(), empty() - Document eg::clear(), size(), component_keys() - Add behavior notes for add/remove/remove_group - Add observer thread safety warning - Fix stale roadmap references in README Example: - Add demos for validate(), entity inspection, multiple observers, weight overrides, independent instances, error handling, clear()
1 parent c5a1374 commit b1d67cd

6 files changed

Lines changed: 657 additions & 53 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Entity Generator for C++
22

3+
> **Requires C++23** (e.g., `-std=c++23` for GCC/Clang, `/std:c++latest` for MSVC).
4+
35
[![Entity Generator for C++](https://raw.githubusercontent.com/dasmig/entity-generator/master/doc/entity-generator.png)](https://github.com/dasmig/entity-generator/releases)
46

57
[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/dasmig/entity-generator/main/LICENSE)
@@ -103,4 +105,4 @@ auto seeded = eg::instance().generate(42);
103105
104106
For the complete feature guide — component dependencies, custom types, seed signatures, batch generation, groups, weights, validation, event hooks, extensions, and more — see the **[Usage Guide](doc/usage.md)**.
105107
106-
For planned features — concurrent generation, conditional components, structured serialization, EnTT integration, and more — see the **[Roadmap](doc/roadmap.md)**.
108+
For planned features — EnTT integration, Python/Node.js/.NET wrappers, and more — see the **[Roadmap](doc/roadmap.md)**.

dasmig/entitygen.hpp

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <iterator>
1111
#include <map>
1212
#include <memory>
13+
#include <optional>
1314
#include <ostream>
1415
#include <ranges>
1516
#include <span>
@@ -125,6 +126,10 @@ class component
125126
{
126127
return std::to_wstring(std::any_cast<float>(value));
127128
}
129+
if (value.type() == typeid(long))
130+
{
131+
return std::to_wstring(std::any_cast<long>(value));
132+
}
128133
if (value.type() == typeid(bool))
129134
{
130135
return std::any_cast<bool>(value) ? L"true" : L"false";
@@ -181,7 +186,11 @@ class choice_component : public component
181186
explicit choice_component(std::wstring key, std::vector<T> choices,
182187
Formatter fmt = {})
183188
: _key{std::move(key)}, _choices{std::move(choices)},
184-
_fmt{std::move(fmt)} {}
189+
_fmt{std::move(fmt)}
190+
{
191+
if (_choices.empty())
192+
throw std::invalid_argument("choices must not be empty");
193+
}
185194

186195
[[nodiscard]] std::wstring key() const override { return _key; }
187196

@@ -296,7 +305,15 @@ class weighted_choice_component : public component
296305
std::vector<option> options,
297306
Formatter fmt = {})
298307
: _key{std::move(key)}, _options{std::move(options)},
299-
_fmt{std::move(fmt)} {}
308+
_fmt{std::move(fmt)}
309+
{
310+
if (_options.empty())
311+
throw std::invalid_argument("options must not be empty");
312+
if (!std::ranges::any_of(_options,
313+
[](const auto& o) { return o.weight > 0.0; }))
314+
throw std::invalid_argument(
315+
"at least one option must have a positive weight");
316+
}
300317

301318
[[nodiscard]] std::wstring key() const override { return _key; }
302319

@@ -395,23 +412,25 @@ class entity
395412
return result;
396413
}
397414

415+
// Number of component values in this entity.
416+
[[nodiscard]] std::size_t size() const { return _entries.size(); }
417+
418+
// Check if the entity has no component values.
419+
[[nodiscard]] bool empty() const { return _entries.empty(); }
420+
398421
// Convert all component values to a single formatted string.
399422
// Each entry is rendered as "key: display" separated by " ".
400423
[[nodiscard]] std::wstring to_string() const
401424
{
402-
std::wstring result;
403-
404-
for (const auto& e : _entries)
405-
{
406-
if (!result.empty())
407-
{
408-
result += L" ";
409-
}
410-
411-
result += e.key + L": " + e.display;
412-
}
425+
auto parts = _entries
426+
| std::views::transform([](const auto& e) {
427+
return e.key + L": " + e.display;
428+
});
413429

414-
return result;
430+
return std::ranges::fold_left_first(parts,
431+
[](std::wstring acc, const std::wstring& part) {
432+
return std::move(acc) + L" " + part;
433+
}).value_or(std::wstring{});
415434
}
416435

417436
// Operator ostream streaming all component values in generation order.
@@ -537,15 +556,15 @@ class eg
537556
const auto key = comp->key();
538557
notify(&generation_observer::on_before_add, key);
539558

540-
auto it = std::ranges::find(_components, key, &component_entry::first);
559+
auto it = std::ranges::find(_components, key, &component_entry::key);
541560

542561
if (it != _components.end())
543562
{
544-
it->second = std::move(comp);
563+
it->comp = std::move(comp);
545564
}
546565
else
547566
{
548-
_components.emplace_back(key, std::move(comp));
567+
_components.push_back({key, std::move(comp)});
549568
}
550569

551570
notify(&generation_observer::on_after_add, key);
@@ -580,8 +599,8 @@ class eg
580599
{
581600
notify(&generation_observer::on_before_remove, component_key);
582601

583-
std::erase_if(_components, [&component_key](const auto& pair) {
584-
return pair.first == component_key;
602+
std::erase_if(_components, [&component_key](const auto& entry) {
603+
return entry.key == component_key;
585604
});
586605
_weight_overrides.erase(component_key);
587606

@@ -592,11 +611,33 @@ class eg
592611
// Check if a component is registered by key.
593612
[[nodiscard]] bool has(const std::wstring& component_key) const
594613
{
595-
return std::ranges::any_of(_components, [&component_key](const auto& pair) {
596-
return pair.first == component_key;
614+
return std::ranges::any_of(_components, [&component_key](const auto& entry) {
615+
return entry.key == component_key;
597616
});
598617
}
599618

619+
// Remove all registered components, weight overrides, and groups.
620+
eg& clear()
621+
{
622+
_components.clear();
623+
_weight_overrides.clear();
624+
_groups.clear();
625+
return *this;
626+
}
627+
628+
// Return the number of registered components.
629+
[[nodiscard]] std::size_t size() const { return _components.size(); }
630+
631+
// Return all registered component keys in registration order.
632+
[[nodiscard]] std::vector<std::wstring> component_keys() const
633+
{
634+
std::vector<std::wstring> keys;
635+
keys.reserve(_components.size());
636+
std::ranges::transform(_components, std::back_inserter(keys),
637+
&component_entry::key);
638+
return keys;
639+
}
640+
600641
// --- Seeding ---------------------------------------------------------
601642

602643
// Seed the internal random engine. Subsequent generate() calls without
@@ -906,8 +947,11 @@ class eg
906947

907948
private:
908949
// Component entry: key + owned component pointer.
909-
using component_entry =
910-
std::pair<std::wstring, std::unique_ptr<component>>;
950+
struct component_entry
951+
{
952+
std::wstring key;
953+
std::unique_ptr<component> comp;
954+
};
911955

912956
// Reference to a component entry for filtered generation.
913957
struct component_ref
@@ -928,7 +972,7 @@ class eg
928972
{
929973
for (const auto& obs : observers)
930974
{
931-
(obs.get()->*hook)(std::forward<Args>(args)...);
975+
((*obs).*hook)(std::forward<Args>(args)...);
932976
}
933977
}
934978

@@ -1067,8 +1111,8 @@ class eg
10671111
refs.reserve(_components.size());
10681112
std::ranges::transform(_components, std::back_inserter(refs),
10691113
[this](const auto& entry) -> component_ref {
1070-
return {entry.first, std::cref(*entry.second),
1071-
effective_weight(entry.first, *entry.second)};
1114+
return {entry.key, std::cref(*entry.comp),
1115+
effective_weight(entry.key, *entry.comp)};
10721116
});
10731117
return refs;
10741118
}
@@ -1082,11 +1126,11 @@ class eg
10821126

10831127
auto matching = _components
10841128
| std::views::filter([&](const auto& entry) {
1085-
return std::ranges::contains(component_keys, entry.first);
1129+
return std::ranges::contains(component_keys, entry.key);
10861130
})
10871131
| std::views::transform([this](const auto& entry) -> component_ref {
1088-
return {entry.first, std::cref(*entry.second),
1089-
effective_weight(entry.first, *entry.second)};
1132+
return {entry.key, std::cref(*entry.comp),
1133+
effective_weight(entry.key, *entry.comp)};
10901134
});
10911135
std::ranges::copy(matching, std::back_inserter(filtered));
10921136

dasmig/ext/stats_observer.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,13 @@ class stats_observer : public generation_observer
429429
}
430430
};
431431

432+
// Stream operator for one-liner report output.
433+
inline std::wostream& operator<<(std::wostream& os,
434+
const stats_observer& stats)
435+
{
436+
return os << stats.report();
437+
}
438+
432439
} // namespace dasmig::ext
433440

434441
#endif // DASMIG_EXT_STATS_OBSERVER_HPP

doc/usage.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ This guide covers every feature of the entity-generator library in detail. For a
2222
- [Event Hooks](#event-hooks)
2323
- [Conditional Components](#conditional-components)
2424
- [Generic Components](#generic-components)
25+
- [Entity Introspection](#entity-introspection)
26+
- [Generator Introspection](#generator-introspection)
2527
- [Extensions](#extensions)
2628

2729
## Defining Components
@@ -64,6 +66,8 @@ class age : public dasmig::component
6466
};
6567
```
6668

69+
`default_to_string()` handles `std::wstring`, `int`, `double`, `float`, `long`, and `bool`. All other types return `[?]` — override `to_string()` for custom types.
70+
6771
## Registering and Generating
6872

6973
```cpp
@@ -93,6 +97,11 @@ auto second = eg::instance().generate();
9397
eg::instance().unseed();
9498
```
9599
100+
**Behavior notes:**
101+
- `add()` with a key that already exists **replaces** the component in place, preserving registration order. Any existing weight override for that key persists.
102+
- `remove()` on a non-existent key is a **no-op** (does not throw).
103+
- `remove_group()` on a non-existent group is a **no-op**.
104+
96105
## Typed Retrieval
97106
98107
```cpp
@@ -102,8 +111,14 @@ auto entity = eg::instance().generate();
102111
std::wstring char_class = entity.get<std::wstring>(L"class");
103112
int char_age = entity.get<int>(L"age");
104113
114+
// Type-erased access (returns const std::any&).
115+
const auto& raw = entity.get_any(L"class");
116+
105117
// Check if a component exists.
106118
if (entity.has(L"name")) { /* ... */ }
119+
120+
// Iterate over all keys in generation order.
121+
for (const auto& key : entity.keys()) { /* ... */ }
107122
```
108123

109124
## Component Dependencies
@@ -469,6 +484,8 @@ eg::instance().clear_observers();
469484
470485
Multiple observers fire in registration order. The same observer can be added more than once.
471486
487+
**Thread safety:** Observers are **not** thread-safe by default. When using `generate_batch_async`, the caller is responsible for ensuring observer implementations are safe for concurrent invocation (e.g., using atomics or mutexes).
488+
472489
The full set of hooks (6 before/after pairs + 3 single-fire hooks, 15 methods):
473490
474491
| Event | Hook(s) |
@@ -586,6 +603,48 @@ gen.add(std::make_unique<dasmig::constant_component<int, decltype(fmt)>>(
586603
// to_map()["id"] == "#5"
587604
```
588605
606+
## Entity Introspection
607+
608+
```cpp
609+
auto entity = eg::instance().generate();
610+
611+
// Number of component values.
612+
std::size_t n = entity.size();
613+
614+
// Check if empty.
615+
if (entity.empty()) { /* ... */ }
616+
617+
// All keys in generation order.
618+
auto keys = entity.keys();
619+
620+
// Type-erased access.
621+
const std::any& val = entity.get_any(L"class");
622+
623+
// Seed of the entity and individual components.
624+
auto entity_seed = entity.seed();
625+
auto comp_seed = entity.seed(L"class");
626+
627+
// Display string map.
628+
auto m = entity.to_map(); // map<wstring, wstring>
629+
```
630+
631+
## Generator Introspection
632+
633+
```cpp
634+
dasmig::eg gen;
635+
gen.add(std::make_unique<age>())
636+
.add(std::make_unique<character_class>());
637+
638+
// Number of registered components.
639+
std::size_t n = gen.size(); // 2
640+
641+
// All registered keys in registration order.
642+
auto keys = gen.component_keys(); // {"age", "class"}
643+
644+
// Remove everything (components, weight overrides, groups).
645+
gen.clear();
646+
```
647+
589648
## Extensions
590649

591650
Optional headers in `dasmig/ext/` provide ready-made functionality built on the observer interface.

0 commit comments

Comments
 (0)