Allow variable NAMES to be declared by pattern
Against main at Env::Assert 0.016. Line numbers and quoted code are from
the working copy, not the CPAN release -- the released file is perltidied and the
repository copy is not.
Depends on "Allow a variable to be declared permitted but not required".
A name pattern cannot mean required -- you cannot demand the presence of a
variable whose name you do not know -- so this only makes sense once optional
variables exist. Please do not start this one first.
Summary
.envdesc declares one literal variable name per line. Add the ability to
declare a family of names by regular expression, so that variables which are
injected by the environment rather than chosen by the author can be tolerated
without enumerating them.
Why enumeration is not enough
The companion issue makes it possible to say "LANG may be absent". That
solves the handful of variables you can name.
It does not solve the ones you cannot. In a Kubernetes pod, the container runtime
injects a variable family for every Service in the namespace:
MYDB_SERVICE_HOST=10.43.0.31
MYDB_SERVICE_PORT=5432
MYDB_PORT=tcp://10.43.0.31:5432
MYDB_PORT_5432_TCP_ADDR=10.43.0.31
MYDB_PORT_5432_TCP_PORT=5432
MYDB_PORT_5432_TCP_PROTO=tcp
...
You cannot list these in advance, because the list is a property of the
namespace at pod start, not of your application. Someone deploying an
unrelated Service alongside yours changes it. With --exact on and only literal
declarations available, every such deployment breaks your startup check, and the
maintenance burden lands on whoever is least equipped to understand it.
One rule should be able to cover the family:
## envassert (var: name_is_pattern=1, optional=1)
^[A-Z0-9_]{1,}_PORT_[0-9]{1,}_TCP(_(ADDR|PORT|PROTO)){0,1}$=^.*$
Secondary benefit, not the motivation
Name patterns are also a way to state a rule across a family you do own:
^APP_FEATURE_[A-Z_]{1,}$=^(0|1)$
"every feature flag must be 0 or 1", without listing them. This works whether or
not --exact is on, and may turn out to be the more commonly used half.
Design decisions
These are the maintainer's to make. I have opinions and no stake; the point of
listing them is that a contributor should not have to guess.
1. Syntax
The current parse takes everything before the first = as the name:
# lib/Env/Assert/Functions.pm:244
^ (?<name> [^=]{1,}) = (?<value> .*) $
so a literal name may currently contain almost anything. Options:
- a tag comment, consistent with the companion issue:
## envassert (var: name_is_pattern=1)
- a sigil:
~^KUBERNETES_.*$=^.*$
- delimiters:
/^KUBERNETES_.*$/=^.*$
The tag comment is the most conservative: it cannot collide with any existing
file, because a line beginning ## is already a comment. A sigil is terser but
theoretically ambiguous, since [^=]{1,} permits a leading ~. In practice
POSIX environment variable names are [A-Za-z_][A-Za-z0-9_]*, so no real
variable starts with ~ or / -- but "in practice" is doing work in that
sentence, and the tag avoids needing it.
2. Anchoring
Decide and document whether a name pattern is anchored implicitly. The value
regexp is applied unanchored today (m/$regexp/msx) and the shipped examples
anchor themselves (^[[:word:]]{1,}$).
If name patterns follow that convention, then _HOST matches SMTP_HOST and
also SMTP_HOSTNAME_OVERRIDE. That will surprise somebody. Either anchor
implicitly, or say loudly in the POD that you must anchor, and prefer the option
that makes the surprising case impossible rather than merely documented.
3. Precedence -- the one that will cause a bug if it is left implicit
SMTP_HOST can match both its own literal line and a broad ^.*_HOST$ rule.
Which value regexp applies?
|
|
| A |
literal wins; among patterns, first in file order wins |
| B |
literal wins; two matching patterns is a parse-time error |
| C |
every matching rule must be satisfied (conjunctive) |
I would choose B. Under A, a permissive family rule can silently loosen a
strict check that was deliberately written, and nothing tells you -- which is the
same class of silent-drift failure that --exact exists to prevent. B refuses to
guess. C is elegant and makes a broad tolerate-rule (^.*$) free, but it means
adding one family rule can retroactively constrain unrelated variables, which is
surprising in the other direction.
Whichever is chosen, the error message should name the rule that matched.
"Variable SMTP_HOST has invalid content" is not enough once more than one rule
could be responsible.
4. A pattern rule cannot be required
Marking one required should be a parse-time error, not silently ignored. It
is a statement that cannot be satisfied, and failing at parse time is the only
place it can be reported usefully.
What changes in the code
Two loops in Env::Assert::Functions::assert (line 133), and they change
differently:
-
The presence loop (line 145) iterates keys %{ $vars } -- declared names.
Pattern rules have no name to look up and must not take part in it: there
is nothing whose presence they could require.
-
The exact-mode loop (line 166) currently does a hash lookup:
# lib/Env/Assert/Functions.pm:168
if( ! exists $vars->{ $var_name } ) {
This is the line that changes: a variable is "known" if it matches a literal
name or any name pattern.
-
The value check needs to apply to variables matched by a pattern rule,
which today it cannot -- they are not in $vars under their own name.
The natural shape is a helper that answers "which rule governs this environment
variable?", used by both the exact-mode loop and the value check, with the
precedence decision from (3) living in that one function.
file_to_desc (line 209) needs to keep pattern rules somewhere separate from the
name-keyed variables hash -- they cannot be keyed by name, since their name is
not a name.
Performance
Not a concern worth designing around: a handful of rules against a few dozen
variables, once, at startup. Compile each pattern once with qr// rather than in
the inner loop and that is the end of it.
Acceptance criteria
- A name pattern in
.envdesc matches multiple environment variables, and each
matched variable has its value checked against that rule's regexp.
- With
--exact, a variable matching a name pattern is not reported as
missing from the description.
- With
--exact, a variable matching no literal and no pattern is still
reported.
- A pattern rule never causes a "missing from environment" error, whatever the
environment contains.
- Marking a pattern rule as required is a parse-time error naming the line.
- The precedence rule chosen in (3) is implemented, documented in the POD, and
covered by a test where a literal and a pattern both match.
- Error messages name the matching rule.
- Existing
.envdesc files behave identically. Nothing without the new marker
changes.
Tests
prove -l, per CONTRIBUTING.md. exact is already exercised in
t/env-assert-private.t and t/env-assert-public-assert.t.
Cases worth writing:
- one pattern, three matching variables, all valid -> pass
- one pattern, one matching variable with a bad value -> error naming that
variable
- pattern +
--exact, matched variables -> not reported as undeclared
- pattern +
--exact, an unmatched variable -> still reported
- literal and pattern both matching one variable -> the behaviour decided in (3)
- pattern marked required -> parse-time error
- a description file with no patterns -> byte-identical behaviour to today
- a realistic Kubernetes-shaped environment (a
KUBERNETES_* set plus one
Service family) declared by two rules, under --exact -> pass
The last one is the acceptance test for the whole feature: it is the scenario
that motivates it, and if it does not read clearly in the test file the syntax
probably needs another look.
Allow variable NAMES to be declared by pattern
Against
mainat Env::Assert 0.016. Line numbers and quoted code are fromthe working copy, not the CPAN release -- the released file is perltidied and the
repository copy is not.
Summary
.envdescdeclares one literal variable name per line. Add the ability todeclare a family of names by regular expression, so that variables which are
injected by the environment rather than chosen by the author can be tolerated
without enumerating them.
Why enumeration is not enough
The companion issue makes it possible to say "
LANGmay be absent". Thatsolves the handful of variables you can name.
It does not solve the ones you cannot. In a Kubernetes pod, the container runtime
injects a variable family for every Service in the namespace:
You cannot list these in advance, because the list is a property of the
namespace at pod start, not of your application. Someone deploying an
unrelated Service alongside yours changes it. With
--exacton and only literaldeclarations available, every such deployment breaks your startup check, and the
maintenance burden lands on whoever is least equipped to understand it.
One rule should be able to cover the family:
Secondary benefit, not the motivation
Name patterns are also a way to state a rule across a family you do own:
"every feature flag must be 0 or 1", without listing them. This works whether or
not
--exactis on, and may turn out to be the more commonly used half.Design decisions
These are the maintainer's to make. I have opinions and no stake; the point of
listing them is that a contributor should not have to guess.
1. Syntax
The current parse takes everything before the first
=as the name:# lib/Env/Assert/Functions.pm:244 ^ (?<name> [^=]{1,}) = (?<value> .*) $so a literal name may currently contain almost anything. Options:
## envassert (var: name_is_pattern=1)~^KUBERNETES_.*$=^.*$/^KUBERNETES_.*$/=^.*$The tag comment is the most conservative: it cannot collide with any existing
file, because a line beginning
##is already a comment. A sigil is terser buttheoretically ambiguous, since
[^=]{1,}permits a leading~. In practicePOSIX environment variable names are
[A-Za-z_][A-Za-z0-9_]*, so no realvariable starts with
~or/-- but "in practice" is doing work in thatsentence, and the tag avoids needing it.
2. Anchoring
Decide and document whether a name pattern is anchored implicitly. The value
regexp is applied unanchored today (
m/$regexp/msx) and the shipped examplesanchor themselves (
^[[:word:]]{1,}$).If name patterns follow that convention, then
_HOSTmatchesSMTP_HOSTandalso
SMTP_HOSTNAME_OVERRIDE. That will surprise somebody. Either anchorimplicitly, or say loudly in the POD that you must anchor, and prefer the option
that makes the surprising case impossible rather than merely documented.
3. Precedence -- the one that will cause a bug if it is left implicit
SMTP_HOSTcan match both its own literal line and a broad^.*_HOST$rule.Which value regexp applies?
I would choose B. Under A, a permissive family rule can silently loosen a
strict check that was deliberately written, and nothing tells you -- which is the
same class of silent-drift failure that
--exactexists to prevent. B refuses toguess. C is elegant and makes a broad tolerate-rule (
^.*$) free, but it meansadding one family rule can retroactively constrain unrelated variables, which is
surprising in the other direction.
Whichever is chosen, the error message should name the rule that matched.
"Variable SMTP_HOST has invalid content" is not enough once more than one rule
could be responsible.
4. A pattern rule cannot be required
Marking one required should be a parse-time error, not silently ignored. It
is a statement that cannot be satisfied, and failing at parse time is the only
place it can be reported usefully.
What changes in the code
Two loops in
Env::Assert::Functions::assert(line 133), and they changedifferently:
The presence loop (line 145) iterates
keys %{ $vars }-- declared names.Pattern rules have no name to look up and must not take part in it: there
is nothing whose presence they could require.
The exact-mode loop (line 166) currently does a hash lookup:
This is the line that changes: a variable is "known" if it matches a literal
name or any name pattern.
The value check needs to apply to variables matched by a pattern rule,
which today it cannot -- they are not in
$varsunder their own name.The natural shape is a helper that answers "which rule governs this environment
variable?", used by both the exact-mode loop and the value check, with the
precedence decision from (3) living in that one function.
file_to_desc(line 209) needs to keep pattern rules somewhere separate from thename-keyed
variableshash -- they cannot be keyed by name, since their name isnot a name.
Performance
Not a concern worth designing around: a handful of rules against a few dozen
variables, once, at startup. Compile each pattern once with
qr//rather than inthe inner loop and that is the end of it.
Acceptance criteria
.envdescmatches multiple environment variables, and eachmatched variable has its value checked against that rule's regexp.
--exact, a variable matching a name pattern is not reported asmissing from the description.
--exact, a variable matching no literal and no pattern is stillreported.
environment contains.
covered by a test where a literal and a pattern both match.
.envdescfiles behave identically. Nothing without the new markerchanges.
Tests
prove -l, perCONTRIBUTING.md.exactis already exercised int/env-assert-private.tandt/env-assert-public-assert.t.Cases worth writing:
variable
--exact, matched variables -> not reported as undeclared--exact, an unmatched variable -> still reportedKUBERNETES_*set plus oneService family) declared by two rules, under
--exact-> passThe last one is the acceptance test for the whole feature: it is the scenario
that motivates it, and if it does not read clearly in the test file the syntax
probably needs another look.