.. Copyright (c) 2026 Tobias Erbsland - Erbsland DEV. https://erbsland.dev SPDX-License-Identifier: Apache-2.0 .. index:: single: Configuration; Validation Rules single: Validation Rules; Embedded Documents single: Validation Rules; C++ Builder ************************************** Validating Documents with Rule Schemas ************************************** Typed getters can verify one setting at a time. Validation rules describe the contract for an entire configuration tree: which paths may appear, which types they hold, which values are optional or secret, and which constraints each value must satisfy. Validation can also insert declared defaults and attach rule metadata for later diagnostics or presentation. Erbsland Core supports the same validation model from two sources. You can write a readable ELCL rules document and compile it into the application, or construct the rules directly in C++ with :cpp:class:`RulesBuilder `. Both approaches produce an immutable :cpp:class:`Rules ` object that can validate many documents. The `ELCL validation-rules documentation `_ is the language reference for rule-document structure, rule types, constraints, alternatives, versions, dependencies, and custom diagnostics. This page focuses on loading and applying those rules with Erbsland Core. Embed a Rules Document ====================== A separate ``.elcl`` file keeps a substantial schema approachable in reviews and lets tools process it without understanding C++. For an application-owned schema, compile that file into the executable with the resource system. This preserves the source file as a normal project artifact while avoiding a deployment-time search path or a mismatch between the executable and its rules. The demo embeds this rules document: .. code-block:: elcl [survey] type: "section" title: "Levantamento da floresta" [survey.forest] type: "text" minimum: 3 maximum: 80 [survey.observers] type: "integer" minimum: 1 maximum: 12 [survey.region] type: "text" default: "Mata Atlântica" [survey.api_token] type: "text" is_optional: yes is_secret: yes Each section path identifies a node in the application document. The ``type`` attribute chooses its ELCL type; other attributes add metadata or behavior. Constraints such as ``minimum`` and ``maximum`` narrow accepted content according to the rule type, so here they bound text length for ``forest`` and numeric value for ``observers``. Add the directory to the demo target with ``erbsland_core_add_resources`` as described in :doc:`/topics/resource/compiled_resources`: .. code-block:: cmake erbsland_core_add_resources( TARGET validation_rules DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/data" IDENTIFIER "conf-validation-rules" SUFFIXES ".elcl" ) At runtime, read the text from :cpp:func:`Application::resources() `, parse it as a normal configuration document, and pass the result to :cpp:func:`Rules::createFromDocument() `. The factory checks that the document itself is a valid rule schema. A malformed definition therefore fails during application setup, before it is applied to user configuration. .. erbsland-demo:: :source: conf/ValidationRules/EmbeddedRules.cpp :exec: conf/validation_rules --demo EmbeddedRules :sha256: 3b4f4bf7e97cba0d902e1acf3413f69583a7c5e8f8d3a23bb3fc1b2baff52f62 .. code-block:: cpp /// Load and apply an ELCL validation-rules document embedded in the executable. /// /// Compiled resources keep a readable rules file beside the source code without introducing a deployment-time path. /// Parse that text, create a reusable `Rules` instance, and validate each application document before reading values. void embeddedRules() { // Load the rules document from the application's compiled resources. const auto ruleText = el::application().resources().getTextOrThrow("conf-validation-rules"_el, "survey-rules.elcl"_el); const auto ruleDocument = el::conf::Parser{}.parseTextOrThrow(ruleText); const auto rules = el::conf::vr::Rules::createFromDocument(ruleDocument); const auto configuration = "[survey]\n" "forest: \"Parque Nacional da Tijuca\"\n" "observers: 8\n"_el; const auto document = el::conf::Parser{}.parseTextOrThrow(configuration); // Validation checks the shape and constraints, then adds configured default values. rules->validate(document, 1); el::io::printLine("Forest: "_el, document->getTextOrThrow("survey.forest"_el)); el::io::printLine("Region: "_el, document->getTextOrThrow("survey.region"_el)); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mForest: Parque Nacional da Tijuca Region: Mata Atlântica␛[0m .. erbsland-demo-end:: Call :cpp:func:`Rules::validate() ` only after the application document has been parsed and before its values are interpreted. The integer argument selects the application's configuration-schema version. Even when the first schema has no version-specific rules, passing its explicit version keeps the loading contract ready for later evolution. Validation stops at the first violation and throws ``ConfError`` with the relevant path and location. On success, defaults such as ``survey.region`` are present in the document and ordinary typed getters can read the validated tree. Create the rules once during setup and reuse them when the application loads or reloads multiple documents. Build Rules into C++ ==================== A schema that closely follows compiled application logic can be expressed with ``RulesBuilder``. This approach makes rule types and builder arguments visible to the C++ compiler and removes the parsing step. It is especially convenient for compact schemas, library-owned configuration branches, or generated rule definitions. :cpp:func:`RulesBuilder::addRule() ` associates a name path with a :cpp:class:`RuleType ` and any number of builder attributes. Attributes such as :cpp:class:`Title `, :cpp:class:`Default `, :cpp:class:`IsOptional `, and :cpp:class:`IsSecret ` describe metadata or behavior. Constraint attributes such as :cpp:class:`Minimum ` and :cpp:class:`Maximum ` restrict accepted values. .. erbsland-demo:: :source: conf/ValidationRules/BuiltRules.cpp :exec: conf/validation_rules --demo BuiltRules :sha256: 6648398d87c78c08df54b08e4a3960ec58c993529c2794c02357ec4436959770 .. code-block:: cpp /// Build and apply validation rules directly in C++. /// /// `RulesBuilder` combines each rule type with attributes and constraints. Attributes describe behavior or metadata; /// constraints narrow the accepted value. Finalization checks the definition and returns an immutable, reusable rule /// set. void builtRules() { using el::conf::vr::RuleType; using el::conf::vr::builder::Default; using el::conf::vr::builder::Description; using el::conf::vr::builder::IsOptional; using el::conf::vr::builder::IsSecret; using el::conf::vr::builder::Maximum; using el::conf::vr::builder::Minimum; using el::conf::vr::builder::Title; auto builder = el::conf::vr::RulesBuilder{}; builder.configureRoot(Title{"Levantamento florestal"_el}); builder.addRule("survey"_el, RuleType::Section, Title{"Levantamento da floresta"_el}); auto survey = builder.createBranch("survey"_el); survey.addRule("forest"_el, RuleType::Text, Description{"Nome da floresta observada"_el}, Minimum{3}, Maximum{80}); survey.addRule("observers"_el, RuleType::Integer, Minimum{1}, Maximum{12}); survey.addRule("region"_el, RuleType::Text, Default{"Mata Atlântica"_el}); survey.addRule("api_token"_el, RuleType::Text, IsOptional{}, IsSecret{}); const auto rules = builder.takeRules(); const auto configuration = "[survey]\n" "forest: \"Floresta Nacional de Ipanema\"\n" "observers: 6\n"_el; const auto document = el::conf::Parser{}.parseTextOrThrow(configuration); rules->validate(document, 1); el::io::printLine("Validated observers: "_el, document->getIntegerOrThrow("survey.observers"_el)); el::io::printLine("Default region: "_el, document->getTextOrThrow("survey.region"_el)); } .. erbsland-demo-run:: .. erbsland-ansi:: :escape-char: ␛ ␛[0mValidated observers: 6 Default region: Mata Atlântica␛[0m .. erbsland-demo-end:: The document root has an implicit rule. Use ``createBranch()`` to pass a subtree to a helper or add multiple children without repeating their parent path. Branches can be nested and passed as ``RulesBuilder&``. They share the builder backend; declare parent rules explicitly and finalize the schema through the main builder. Use :cpp:func:`configureRoot() ` when the root itself needs a title, description, dependency, version range, or another compatible attribute. Use :cpp:func:`addAlternative() ` when one path deliberately accepts several rule definitions. :cpp:func:`takeRules() ` finalizes the schema and resets the builder for reuse. Finalization checks the relationships between definitions and rejects incompatible attributes, invalid references, and other logical errors before configuration validation begins. The resulting ``Rules`` object is applied exactly like one created from a rules document, so the choice of source does not spread into the rest of the loading code. Choose the Rule Source ====================== Prefer an embedded ELCL document when people need to read or maintain a substantial schema independently of C++. It remains normal text in the source tree, while compiled resources keep deployment reliable. Prefer ``RulesBuilder`` when a compact schema belongs directly to a component's implementation or when C++ code generates definitions from another model. Applications can also combine these approaches at subsystem boundaries. For example, the main executable can own an embedded application schema while a reusable component exposes a built rule set for the configuration branch it understands. Whichever source you choose, finalize the rules during setup, validate the complete input, and only then interpret the document. Continue with Validated Values ============================== Successful validation leaves useful information on each value: the matching rule, secret status, default status, and whether the value passed through validation at all. The next topic explains how application diagnostics, configuration dumps, and editors can use this metadata safely.