Validation Rules
Validation rules check an entire configuration before the application uses it. They describe allowed names, value types, constraints, defaults, and relationships between values. A schema is closed: values without a matching rule are rejected.
Define rules in Python or in an ELCL document. Both forms use the same compiler and validation engine. Compile rules once and reuse them for multiple documents.
Building Rules in Python
Use native Python scalar types and keyword arguments:
import erbsland.conf as elcl
from erbsland.conf import vr
builder = vr.RulesBuilder()
server = builder.add_rule("server", vr.RuleType.SECTION, title="Server settings")
server.add_rule("port", int, minimum=1, maximum=65535, default=8080)
server.add_rule("mode", str, in_=["http", "https"])
server.add_rule(
"tags",
vr.RuleType.VALUE_LIST,
entry_type=str,
entry_options={"minimum": 2},
is_optional=True,
)
rules = builder.build()
document = elcl.loads('[server]\nmode: "https"\n')
rules.validate(document, version=1)
assert document.get_int("server.port") == 8080
build() checks the whole definition and returns an immutable snapshot.
It does not reset the builder. Subsequent edits cannot change previously built
rules. Invalid definitions and document validation failures raise
ConfValidationError.
Declare parent sections before their children. add_rule() returns a branch
view, whose paths are relative to that rule. builder.branch("server") retrieves
an existing branch. configure_root() updates the rule represented by a view.
Only the original builder can build() or reset(); resetting invalidates
existing branch views.
Native scalar types include int, bool, float, str, bytes,
datetime.date, datetime.time, datetime.datetime, datetime.timedelta,
re.Pattern, and the library’s Time, DateTime and TimeDelta types.
Use RuleType for sections, lists, matrices, any scalar (VALUE), and
unvalidated branches (NOT_VALIDATED). ELCL type identifiers such as
"value_list" are accepted too. Values are checked strictly: a boolean does
not satisfy an integer rule.
Reading ELCL Rules
The same schema can be written as ordinary ELCL:
[server]
type: "section"
title: "Server settings"
[server.port]
type: "integer"
minimum: 1
maximum: 65535
default: 8080
[server.mode]
type: "text"
in: "http", "https"
[server.tags]
type: "ValueList"
is_optional: Yes
[server.tags.vr_entry]
type: "text"
minimum: 2
Load a rules file with vr.Rules.load("rules.elcl") or text with
vr.Rules.loads(text). To use a parser with customized source resolution,
access policies or signature handling, parse the rules normally and pass the
result to vr.Rules.from_document(document).
ELCL rules support templates, alternatives, implicit ancestor sections,
variable names, keys and dependencies. The ELCL validation specification
describes the language. List and matrix definitions require an explicit
vr_entry; Python’s entry_type= creates this entry rule.
Literal Configuration Names
An attribute such as type cannot share an ELCL section with a child section
of the same name. Escape that child declaration with vr_literal_:
[server]
type: "section"
title: "Server settings"
[server.vr_literal_type]
type: "text"
[server.vr_literal_title]
type: "text"
default: "Main server"
These rules target server.type and server.title. Exactly one prefix is
removed: vr_literal_vr_any targets a field literally named vr_any.
The legacy escape vr_vr_type targets vr_type. Declarations resolving to
the same name are rejected, including collisions introduced through templates.
Python builder paths are always literal. Use server.add_rule("type", str)
directly. A Python declaration named vr_literal_type targets that exact
configuration name.
Dependencies and index paths always use actual configuration names.
In key paths, vr_entry is structural only immediately after a section-list
rule. A list literally named vr_entry can therefore be indexed using
"vr_entry.vr_entry.type".
Entries, Alternatives and Variable Names
Use dedicated helpers to keep structural roles separate from literal names:
builder = vr.RulesBuilder()
users = builder.add_rule("users", vr.RuleType.SECTION_LIST)
user = users.add_entry(vr.RuleType.SECTION)
user.add_rule("name", str, minimum=1)
settings = builder.add_rule("settings", vr.RuleType.SECTION)
settings.add_rule("vr_any", str, default="literal field")
variable = settings.add_any(int, minimum=0)
variable.configure_name(starts="counter_")
settings.add_alternative("port", int, minimum=1, maximum=65535)
settings.add_alternative("port", str, equals="auto")
add_entry_alternative() and add_any_alternative() create alternatives
for entries and wildcard rules. configure_name() applies text constraints to
the matched name; it also works on ordinary named rules.
Alternatives are tried in declaration order. The first whose own type, version and constraints match is selected. Child validation follows selection and does not backtrack to another alternative.
ValueList accepts a scalar as a one-entry list. ValueMatrix accepts a
scalar as a one-by-one matrix and a flat list as a one-column matrix. This does
not change the stored value type. Matrix size constraints take a pair of integers:
rows followed by columns. Column checks apply to every row, including ragged rows.
Negated matrix constraints negate each dimension’s check.
Constraints and Diagnostics
Keyword names match ELCL: minimum, maximum, equals, multiple,
starts, ends, contains, chars, matches, and key.
Use in_ for the ELCL in constraint because in is a Python keyword.
Supported not_ forms negate constraints; not_chars forbids any occurrence
of the specified characters.
Constraints run in keyword order, after type checking. Attach a constraint error
with, for example, minimum_error="Choose a positive port". The corresponding
constraint must exist. Constraint errors override the node-level error;
neither overrides structural or type errors. Empty custom messages are ignored.
Text comparisons use Unicode str.casefold() unless case_sensitive=True.
Character sets are always case-sensitive. matches uses Python’s re
dialect and partial matching via search(); its flags are independent of
case_sensitive. Python declarations accept a pattern string or compiled
re.Pattern; ELCL declarations require a regex literal. Anchor patterns when
a whole-value match is intended. Python’s regex engine does not impose a timeout.
Floating equality and membership use math.isclose with relative and absolute
tolerances of eight times sys.float_info.epsilon. Floating multiples compare
the quotient to the nearest integer using the same tolerances. NaN matches NaN
for equality, but cannot satisfy a minimum or maximum. Zero and nonfinite
divisors are rejected. Text lengths count Unicode code points, not encoded bytes.
Date-time bounds compare instants with nanosecond precision. Naive Python date-times and ELCL date-times without an offset are interpreted as UTC for these comparisons; explicit offsets are respected.
Indexes and Dependencies
builder.add_key("users.vr_entry.name", name="user_names")
settings.add_rule("owner", str, key="user_names", is_optional=True)
settings.add_rule("token", str, is_optional=True, is_secret=True)
settings.add_dependency(
vr.DependencyMode.IF, source="owner", target="token",
error="An owner requires a token.",
)
add_key() enforces uniqueness in a section list. Multiple paths form a
composite key; all paths must address the same list. A named index is visible
inside the branch declaring it, with nearer definitions shadowing ancestors.
References can select a composite component using "index_name[0]" or refer
to the whole composite as comma-separated text. Index case sensitivity governs
both uniqueness and reference matching.
Dependencies support IF, IF_NOT, OR, XOR, XNOR and AND.
Source and target each accept one path or a sequence of paths; each side is
configured if any referenced node was explicitly supplied. Defaults do not count.
Paths are relative to the declaring branch and must reference conditionally
present nodes, without crossing lists or alternative definitions.
Mutation, Reuse and Metadata
rules.validate(document_or_section, version=1) requires an explicit,
nonnegative integer version and returns None on success. The application
chooses the version; it is separate from ELCL’s @version.
Validation inserts defaults and attaches metadata in place. Default values must match the declared type but bypass ordinary constraints. Defaults and optionality cannot be combined. A missing optional parent does not create default children.
Each value exposes:
validation_rule: the selected read-only rule, orNone.is_validated: whether the node has passed local validation.is_default_value: whether validation inserted it as a default.is_secret: whether the selected rule marks it as sensitive.
Rule metadata includes title, description, constraints, children, location and
name_path, plus default and read-only version_constraints.
Rule paths are symbolic and relative to the validated root:
items.vr_entry.type represents the field in any list entry. Value paths
retain concrete list indexes and their full document ancestry.
Validation failures preserve source locations and concrete configuration paths.
Generated diagnostics do not include supplied secret values, and str(value)
masks values marked secret. Native getters intentionally still return their
contents; conformance-test serializers are intended for trusted test data.
Revalidation removes previous defaults and refreshes metadata only within the selected subtree. Rule objects can be shared between independent validations; their indexes and traversal state are per call. Do not concurrently validate the same mutable document. Failure can leave partial metadata or defaults; successful validation of the whole document is required before using it.