DNDSR 0.1.0.dev1+gcd065ad
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
ConfigRegistry.hpp
Go to the documentation of this file.
1#pragma once
2/// @file ConfigRegistry.hpp
3/// @brief Per-type configuration field registry with JSON serialization,
4/// JSON Schema (draft-07) emission, and cross-field validation.
5///
6/// @details
7/// ## Overview
8///
9/// `ConfigRegistry<T>` is a singleton that stores, for each config struct `T`:
10///
11/// - An ordered list of `FieldMeta` descriptors (one per JSON-visible member).
12/// - Lists of cross-field checks (context-free and context-aware).
13///
14/// The registry is populated lazily on first use by the `DNDS_DECLARE_CONFIG`
15/// machinery in ConfigParam.hpp. It never affects the layout or trivial
16/// copyability of `T` itself --- all metadata lives in static singletons,
17/// so `T` remains a POD struct safe for CUDA device views.
18///
19/// ## Design Principles
20///
21/// 1. **POD structs are untouched.** `DNDS_DECLARE_CONFIG` generates only
22/// static methods and friend functions. The struct has no base class, no
23/// virtual methods, and no added instance data. Structs embedded in CUDA
24/// device views (e.g. `FiniteVolumeSettings`) remain trivially copyable.
25///
26/// 2. **Type-erased field accessors.** Each `FieldMeta` stores `std::function`
27/// lambdas for read, write, and schema emission. These capture a
28/// pointer-to-member and live only in host-side static storage.
29///
30/// 3. **Per-field + cross-field validation.** Single-field constraints (range,
31/// enum membership) are checked inside each field's `readField` closure at
32/// parse time. Multi-field constraints (mutual exclusion, conditional
33/// requirements, derived-value consistency) are registered as standalone
34/// check lambdas via `config.check()` / `config.check_ctx()`.
35///
36/// 4. **Context-aware validation.** Some checks depend on runtime values not
37/// stored in the config (e.g. `nVars`, `model`). These use `ConfigContext`,
38/// passed to `validateWithContext()`.
39///
40/// 5. **Incremental adoption.** Sections using the old
41/// `DNDS_NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_ORDERED_JSON` keep working.
42/// The `--emit-schema` flag falls back to type-inference from default JSON
43/// for unmigrated sections. Migrated and unmigrated sections coexist.
44///
45/// ## Usage
46///
47/// Users interact with this file indirectly through ConfigParam.hpp:
48///
49/// @code
50/// struct TimeMarchControl
51/// {
52/// real dtImplicit = 1e100;
53/// int nTimeStep = 1000000;
54///
55/// DNDS_DECLARE_CONFIG(TimeMarchControl)
56/// {
57/// DNDS_FIELD(dtImplicit, "Max time step; 1e100 for steady",
58/// DNDS::Config::range(0));
59/// DNDS_FIELD(nTimeStep, "Max number of time steps",
60/// DNDS::Config::range(1));
61/// }
62/// };
63/// @endcode
64///
65/// Direct registry access (for tooling, `--emit-schema`, etc.):
66///
67/// @code
68/// auto schema = DNDS::ConfigRegistry<TimeMarchControl>::emitSchema("Time march");
69/// auto errors = DNDS::ConfigRegistry<TimeMarchControl>::validate(myConfig);
70/// @endcode
71
72#include "Defines.hpp"
73#include "JsonUtil.hpp"
74#include "Errors.hpp"
75
76#include <string>
77#include <vector>
78#include <map>
79#include <functional>
80#include <optional>
81#include <stdexcept>
82
83#include <fmt/core.h>
84
85namespace DNDS
86{
87
88 /// @brief Enumerates the JSON Schema type associated with a config field.
89 ///
90 /// Used by FieldMeta::schemaEntry to emit the correct `"type"` value in the
91 /// generated JSON Schema. The mapping is:
92 ///
93 /// | Tag | JSON Schema type | C++ types |
94 /// |----------------|------------------|-----------------------------------|
95 /// | Bool | "boolean" | bool |
96 /// | Int | "integer" | int, int32_t, int64_t, uint8_t |
97 /// | Real | "number" | real (double), float |
98 /// | String | "string" | std::string |
99 /// | Enum | "string" + enum | any enum with DNDS_DEFINE_ENUM_JSON|
100 /// | Array | "array" | std::vector<scalar>, Eigen::VectorXd |
101 /// | Object | "object" | nested config section |
102 /// | ArrayOfObjects | "array" of objects | std::vector<Section> |
103 /// | MapOfObjects | "object" (additionalProperties) | std::map<string,Section> |
104 /// | Json | {} (any) | nlohmann::ordered_json |
105 enum class ConfigTypeTag
106 {
107 Bool,
108 Int,
109 Real,
110 String,
111 Enum,
112 Array,
113 Object,
116 Json,
117 };
118
119 /// @brief Result of a single validation check.
120 ///
121 /// @param passed true if the check passed, false otherwise.
122 /// @param message Human-readable error description. Empty when passed is true.
123 /// Should include the field name(s) involved and the violated
124 /// constraint for clear diagnostics.
126 {
127 bool passed;
128 std::string message;
129 };
130
131 /// @brief Runtime context supplied to context-aware validation checks.
132 ///
133 /// Some cross-field validations depend on values that are not stored in the
134 /// config section itself --- for example, `nVars` depends on the EulerModel
135 /// template parameter and is only known at runtime.
136 ///
137 /// Pass a populated ConfigContext to `validateWithContext()`.
138 /// Checks registered via `config.check()` ignore this struct.
139 /// Checks registered via `config.check_ctx()` receive it as a second argument.
141 {
142 int nVars = -1; ///< Number of solution variables (model-dependent).
143 int dim = -1; ///< Spatial dimension (2 or 3).
144 int gDim = -1; ///< Geometric dimension (2 or 3).
145 int modelCode = -1; ///< Integer code identifying the EulerModel enum value.
146 };
147
148 /// @brief Descriptor for a single configuration field.
149 ///
150 /// Stores everything needed to serialize, deserialize, validate, and generate
151 /// JSON Schema for one member of a config struct. Created by
152 /// `ConfigSectionBuilder::field()` (typically via the `DNDS_FIELD` macro)
153 /// and stored in the per-type `ConfigRegistry<T>` singleton.
154 ///
155 /// All `std::function` members capture a pointer-to-member and description
156 /// string. They live in host-side static storage and never reference
157 /// instance state.
159 {
160 std::string name; ///< JSON key name (may differ from C++ member name for aliased fields).
161 std::string description; ///< Human-readable description, used in JSON Schema and generated docs.
162 ConfigTypeTag typeTag; ///< JSON Schema type category.
163
164 /// @brief Read this field from a JSON object into a struct instance.
165 /// @param j The JSON object to read from (must contain `name` as a key).
166 /// @param obj Pointer to the struct instance (type-erased as void*).
167 /// @throws nlohmann::json::out_of_range if the key is missing.
168 /// @throws nlohmann::json::type_error if the JSON value type doesn't match.
169 std::function<void(const nlohmann::ordered_json &j, void *obj)> readField;
170
171 /// @brief Write this field from a struct instance into a JSON object.
172 /// @param j The JSON object to write into.
173 /// @param obj Pointer to the struct instance (type-erased as const void*).
174 std::function<void(nlohmann::ordered_json &j, const void *obj)> writeField;
175
176 /// @brief Emit the JSON Schema fragment for this field.
177 /// @return An ordered_json object like `{"type":"number","default":1e100,"description":"..."}`.
178 ///
179 /// For enum fields, also includes `"enum": [...]`.
180 /// For object fields, delegates to the nested section's `ConfigRegistry::emitSchema()`.
181 /// For array fields, includes `"items": {...}`.
182 std::function<nlohmann::ordered_json()> schemaEntry;
183
184 /// @brief Allowed string values for enum fields. Empty for non-enum fields.
185 std::vector<std::string> enumValues;
186
187 /// @brief Optional minimum constraint for numeric fields (used in schema + validation).
188 std::optional<double> minimum;
189
190 /// @brief Optional maximum constraint for numeric fields (used in schema + validation).
191 std::optional<double> maximum;
192
193 /// @brief Auxiliary key-value info (emitted as `"x-<key>": "<value>"` in schema).
194 /// Use for units, references, version notes, etc.
195 std::map<std::string, std::string> auxInfo;
196 };
197
198 /// @brief A cross-field validation check that does not require runtime context.
199 ///
200 /// The function receives a const void* pointing to the config section struct.
201 /// It should cast to `const T&` and inspect the fields, returning CheckResult.
202 using CrossFieldCheck = std::function<CheckResult(const void *obj)>;
203
204 /// @brief A cross-field validation check that requires runtime context.
205 ///
206 /// The function receives a const void* pointing to the config section struct
207 /// and a ConfigContext carrying runtime values (nVars, dim, model, etc.).
208 using ContextualCheck = std::function<CheckResult(const void *obj, const ConfigContext &ctx)>;
209
210 /// @brief Per-type singleton registry of config field metadata and validation checks.
211 ///
212 /// @tparam T The config section struct type (e.g. LimiterControl, VRSettings).
213 ///
214 /// ## Thread Safety
215 ///
216 /// Registration happens lazily on first use of `to_json`, `from_json`, or
217 /// `schema()` (via `_dnds_ensure_registered()`). After the one-time init
218 /// completes, the registry is read-only. All const accessors and operations
219 /// (`readFromJson`, `writeToJson`, `emitSchema`, `validate`) are safe to
220 /// call concurrently from multiple threads.
221 ///
222 /// ## Registration Order
223 ///
224 /// Fields are stored in the order the `DNDS_FIELD` / `config.field()` calls
225 /// execute inside the `_dnds_do_register()` body, which matches the source
226 /// order. This gives deterministic JSON key ordering and schema property
227 /// ordering.
228 template <typename T>
230 {
231 /// @brief Mutable access to the field list (used only during static init).
232 static std::vector<FieldMeta> &fieldsMut()
233 {
234 static std::vector<FieldMeta> fs;
235 return fs;
236 }
237
238 /// @brief Mutable access to the context-free check list.
239 static std::vector<CrossFieldCheck> &checksMut()
240 {
241 static std::vector<CrossFieldCheck> cs;
242 return cs;
243 }
244
245 /// @brief Mutable access to the context-aware check list.
246 static std::vector<ContextualCheck> &ctxChecksMut()
247 {
248 static std::vector<ContextualCheck> cs;
249 return cs;
250 }
251
252 /// @brief Optional post-read hook called after readFromJson completes.
253 static std::function<void(T &)> &postReadHookMut()
254 {
255 static std::function<void(T &)> hook;
256 return hook;
257 }
258
259 public:
260 // ================================================================
261 // Registration API (called by ConfigSectionBuilder during lazy init)
262 // ================================================================
263
264 /// @brief Register a single field's metadata.
265 /// @param meta The field descriptor to store.
266 /// @return Always true (return value kept for legacy compatibility).
267 static bool registerField(FieldMeta meta)
268 {
269 fieldsMut().push_back(std::move(meta));
270 return true;
271 }
272
273 /// @brief Register a cross-field validation check (no runtime context needed).
274 /// @param check Lambda taking `const void*` and returning CheckResult.
275 /// @return Always true.
277 {
278 checksMut().push_back(std::move(check));
279 return true;
280 }
281
282 /// @brief Register a cross-field validation check that needs runtime context.
283 /// @param check Lambda taking `const void*` and `const ConfigContext&`, returning CheckResult.
284 /// @return Always true.
286 {
287 ctxChecksMut().push_back(std::move(check));
288 return true;
289 }
290
291 /// @brief Register a post-read hook called after all fields are deserialized.
292 /// Useful for recomputing derived quantities (e.g. CpGas from gamma and Rgas).
293 static void registerPostReadHook(std::function<void(T &)> hook)
294 {
295 postReadHookMut() = std::move(hook);
296 }
297
298 // ================================================================
299 // Read-only accessors (safe to call from any thread after main starts)
300 // ================================================================
301
302 /// @brief All registered field descriptors, in declaration order.
303 static const std::vector<FieldMeta> &fields() { return fieldsMut(); }
304
305 /// @brief All registered context-free checks.
306 static const std::vector<CrossFieldCheck> &checks() { return checksMut(); }
307
308 /// @brief All registered context-aware checks.
309 static const std::vector<ContextualCheck> &contextualChecks() { return ctxChecksMut(); }
310
311 // ================================================================
312 // JSON Serialization
313 // ================================================================
314
315 /// @brief Deserialize all registered fields from a JSON object into a struct.
316 ///
317 /// For each field, reads `j[field.name]` and writes it into the
318 /// corresponding member of `obj`. If the field has a range constraint
319 /// (from `DNDS::Config::range()`), the parsed numeric value is checked
320 /// against min/max bounds before assignment.
321 ///
322 /// @param j Source JSON object.
323 /// @param obj Destination struct instance.
324 /// @throws std::runtime_error on missing keys, type mismatch, or
325 /// range constraint violation.
326 static void readFromJson(const nlohmann::ordered_json &j, T &obj)
327 {
328 for (const auto &f : fields())
329 {
330 try
331 {
332 f.readField(j, &obj);
333 }
334 catch (const std::exception &e)
335 {
336 throw std::runtime_error(
337 fmt::format("Error reading config field '{}': {}", f.name, e.what()));
338 }
339 }
340 auto &hook = postReadHookMut();
341 if (hook)
342 hook(obj);
343 }
344
345 /// @brief Serialize all registered fields from a struct into a JSON object.
346 ///
347 /// Fields are written in registration order, producing deterministic key ordering
348 /// in the output JSON.
349 ///
350 /// @param j Destination JSON object (existing keys are overwritten).
351 /// @param obj Source struct instance.
352 static void writeToJson(nlohmann::ordered_json &j, const T &obj)
353 {
354 for (const auto &f : fields())
355 f.writeField(j, &obj);
356 }
357
358 // ================================================================
359 // JSON Schema Generation
360 // ================================================================
361
362 /// @brief Emit a JSON Schema (draft-07) object describing all registered fields.
363 ///
364 /// The output looks like:
365 /// @code
366 /// {
367 /// "type": "object",
368 /// "description": "...",
369 /// "properties": {
370 /// "dtImplicit": { "type": "number", "default": 1e100, "description": "..." },
371 /// "nTimeStep": { "type": "integer", "default": 1000000, "description": "..." },
372 /// ...
373 /// }
374 /// }
375 /// @endcode
376 ///
377 /// @param sectionDescription Optional description for the section itself.
378 /// @return The schema JSON object.
379 static nlohmann::ordered_json emitSchema(const std::string &sectionDescription = "")
380 {
381 nlohmann::ordered_json schema;
382 schema["type"] = "object";
383 if (!sectionDescription.empty())
384 schema["description"] = sectionDescription;
385 auto &props = schema["properties"] = nlohmann::ordered_json::object();
386 for (const auto &f : fields())
387 props[f.name] = f.schemaEntry();
388 return schema;
389 }
390
391 // ================================================================
392 // Validation
393 // ================================================================
394
395 /// @brief Run all context-free cross-field checks on a struct instance.
396 ///
397 /// @param obj The struct to validate.
398 /// @return A vector of failed CheckResults. Empty if all checks pass.
399 static std::vector<CheckResult> validate(const T &obj)
400 {
401 std::vector<CheckResult> failures;
402 for (const auto &check : checks())
403 {
404 auto r = check(static_cast<const void *>(&obj));
405 if (!r.passed)
406 failures.push_back(std::move(r));
407 }
408 return failures;
409 }
410
411 /// @brief Run all checks (both context-free and context-aware) on a struct instance.
412 ///
413 /// @param obj The struct to validate.
414 /// @param ctx Runtime context (nVars, dim, model, etc.).
415 /// @return A vector of failed CheckResults. Empty if all checks pass.
416 static std::vector<CheckResult> validateWithContext(const T &obj, const ConfigContext &ctx)
417 {
418 auto failures = validate(obj);
419 for (const auto &check : contextualChecks())
420 {
421 auto r = check(static_cast<const void *>(&obj), ctx);
422 if (!r.passed)
423 failures.push_back(std::move(r));
424 }
425 return failures;
426 }
427
428 // ================================================================
429 // Key Validation (unknown-key detection)
430 // ================================================================
431
432 /// @brief Check that every key in a user-supplied JSON object corresponds to
433 /// a registered field. Throws on the first unknown key found.
434 ///
435 /// This is the equivalent of EulerP's `valid_patch_keys()`, but generated
436 /// automatically from the registry instead of requiring a hand-written
437 /// default config to compare against.
438 ///
439 /// @param userJson The user-supplied JSON object to validate.
440 /// @throws std::runtime_error with the offending key path.
441 static void validateKeys(const nlohmann::ordered_json &userJson)
442 {
443 if (!userJson.is_object())
444 return;
445 for (const auto &[key, val] : userJson.items())
446 {
447 bool found = false;
448 for (const auto &f : fields())
449 {
450 if (f.name == key)
451 {
452 found = true;
453 break;
454 }
455 }
456 if (!found)
457 {
458 throw std::runtime_error(
459 fmt::format("Unknown configuration key '{}'. Check spelling or "
460 "use --emit-schema to see valid keys.",
461 key));
462 }
463 }
464 }
465 };
466
467} // namespace DNDS
Core type aliases, constants, and metaprogramming utilities for the DNDS framework.
Assertion / error-handling macros and supporting helper functions.
JSON-to-Eigen conversion utilities and nlohmann_json helper macros.
Core 2D variable-length array container, the storage foundation of DNDSR.
Definition Array.hpp:97
Per-type singleton registry of config field metadata and validation checks.
static const std::vector< ContextualCheck > & contextualChecks()
All registered context-aware checks.
static bool registerField(FieldMeta meta)
Register a single field's metadata.
static bool registerCheck(CrossFieldCheck check)
Register a cross-field validation check (no runtime context needed).
static const std::vector< CrossFieldCheck > & checks()
All registered context-free checks.
static std::vector< CheckResult > validateWithContext(const T &obj, const ConfigContext &ctx)
Run all checks (both context-free and context-aware) on a struct instance.
static void readFromJson(const nlohmann::ordered_json &j, T &obj)
Deserialize all registered fields from a JSON object into a struct.
static const std::vector< FieldMeta > & fields()
All registered field descriptors, in declaration order.
static bool registerContextualCheck(ContextualCheck check)
Register a cross-field validation check that needs runtime context.
static void validateKeys(const nlohmann::ordered_json &userJson)
Check that every key in a user-supplied JSON object corresponds to a registered field....
static nlohmann::ordered_json emitSchema(const std::string &sectionDescription="")
Emit a JSON Schema (draft-07) object describing all registered fields.
static void writeToJson(nlohmann::ordered_json &j, const T &obj)
Serialize all registered fields from a struct into a JSON object.
static std::vector< CheckResult > validate(const T &obj)
Run all context-free cross-field checks on a struct instance.
static void registerPostReadHook(std::function< void(T &)> hook)
Register a post-read hook called after all fields are deserialized. Useful for recomputing derived qu...
the host side operators are provided as implemented
std::function< CheckResult(const void *obj)> CrossFieldCheck
A cross-field validation check that does not require runtime context.
std::function< CheckResult(const void *obj, const ConfigContext &ctx)> ContextualCheck
A cross-field validation check that requires runtime context.
ConfigTypeTag
Enumerates the JSON Schema type associated with a config field.
Result of a single validation check.
Runtime context supplied to context-aware validation checks.
int modelCode
Integer code identifying the EulerModel enum value.
int nVars
Number of solution variables (model-dependent).
int gDim
Geometric dimension (2 or 3).
int dim
Spatial dimension (2 or 3).
Descriptor for a single configuration field.
std::function< void(nlohmann::ordered_json &j, const void *obj)> writeField
Write this field from a struct instance into a JSON object.
ConfigTypeTag typeTag
JSON Schema type category.
std::function< void(const nlohmann::ordered_json &j, void *obj)> readField
Read this field from a JSON object into a struct instance.
std::vector< std::string > enumValues
Allowed string values for enum fields. Empty for non-enum fields.
std::optional< double > minimum
Optional minimum constraint for numeric fields (used in schema + validation).
std::map< std::string, std::string > auxInfo
Auxiliary key-value info (emitted as "x-<key>": "<value>" in schema). Use for units,...
std::string description
Human-readable description, used in JSON Schema and generated docs.
std::function< nlohmann::ordered_json()> schemaEntry
Emit the JSON Schema fragment for this field.
std::optional< double > maximum
Optional maximum constraint for numeric fields (used in schema + validation).
std::string name
JSON key name (may differ from C++ member name for aliased fields).
tVec r(NCells)