NAME
App::Actions::Runner::Support::Env - the env context that resolves variables when they are read
SYNOPSIS
Resolve a set of variables that refer to each other:
use App::Actions::Expression;
use App::Actions::Runner::Support::Env;
my $expression = App::Actions::Expression->new;
my %raw = (A => 'a', B => '${{ env.A }}b');
my %resolved;
my %context;
my $state = tie my %env, 'App::Actions::Runner::Support::Env', \%raw, \%resolved, sub {
return $expression->interpolate($raw{ $_[0] }, \%context);
};
$context{env} = \%env;
print $env{B}, "\n"; # ab
Find out what went wrong:
my $ok = eval { my $value = $env{B}; 1 };
print $state->failure->message, "\n" unless $ok;
Clean up (the hash and the resolver refer to each other):
undef $state;
untie %env;
Most code does not use this class directly: it calls "resolve_env" in App::Actions::Runner::Support, which does all of the above.
DESCRIPTION
A workflow's env values may use other variables: B: ${{ env.A }}b. To work out B, the evaluator must first know A.
This class is a tied hash for the env part of an expression context. When the evaluator reads env.A, the hash works out A at that moment, by calling a function you give it, and keeps the result. So the variables can be read in any order.
Each variable's text is worked out exactly once. Its result is never worked out again, so ${{ }} text that came from somewhere else, such as a commit message, stays as text.
If A is read again while A is still being worked out, the variables form a loop (A uses B, B uses A). That is the error runner.env_cycle.
The hash can only be read. It holds the names of the raw variables, so exists, keys and a case-insensitive search all work.
METHODS
TIEHASH
my $state = tie my %env, 'App::Actions::Runner::Support::Env', \%raw, \%resolved, $resolve;
Called by tie.
ARGUMENTS
\%raw- the variables' text, name to string.\%resolved- an empty hash; each result is stored here.$resolve- a code reference. It is called with a name and must return that variable's value. It usually interpolates$raw{$name}with a context whoseenvis this tied hash.
RETURNS
The tie object. Keep it if you want to call "failure".
SIDE EFFECTS
None.
EXAMPLE
my $state = tie my %env, 'App::Actions::Runner::Support::Env', { A => 'x' }, {}, sub { 'x' };
API SPECIFICATION
INPUT
{
raw => { type => 'hashref' },
resolved => { type => 'hashref' },
resolve => { type => 'coderef' },
}
OUTPUT
{ type => 'object', isa => 'App::Actions::Runner::Support::Env' }
MESSAGES
None.
FETCH
my $value = $env{NAME};
Returns the value of a variable, working it out the first time.
ARGUMENTS
$key- the variable's name, exactly as written (the evaluator does the case-insensitive search by walking the keys).
RETURNS
The value, or undef for a name that is not one of the variables.
SIDE EFFECTS
Stores the value in the \%resolved hash. On a failure, remembers it (see "failure").
EXAMPLE
my $value = $env{B};
API SPECIFICATION
INPUT
{ key => { type => 'string' } }
OUTPUT
{ type => 'string', optional => 1 }
MESSAGES
+-----------------------------+------------------------------+-----------------------------+
| Message | Meaning | Resolution |
+-----------------------------+------------------------------+-----------------------------+
| Environment variable 'X' | X refers to itself, or to a | Break the loop: give one of |
| refers to itself, directly | variable that refers back | the variables a value that |
| or through other variables | to it | does not use the others |
+-----------------------------+------------------------------+-----------------------------+
| Unable to evaluate | The resolver failed for X | Fix the expression in the |
| environment variable 'X': E | | workflow's env |
+-----------------------------+------------------------------+-----------------------------+
EXISTS, FIRSTKEY, NEXTKEY and SCALAR
exists $env{NAME};
my @names = keys %env;
if (%env) { ... }
These see the raw variables, so a name is there before it is worked out. Walking the keys does not work any variable out.
API SPECIFICATION
INPUT
{ key => { type => 'string', optional => 1 } }
OUTPUT
{ type => 'string', optional => 1 }
MESSAGES
None.
failure
my $error = $state->failure;
Returns the first error any read raised, or undef. The evaluator turns an error inside a lookup into plain text; this keeps the original App::Actions::Error object with its code.
API SPECIFICATION
INPUT
{}
OUTPUT
{ type => 'object', isa => 'App::Actions::Error', optional => 1 }
MESSAGES
None.
fail
$state->fail('runner.env_cycle', { name => 'A' });
Raises a catalogue error and remembers it as the first failure. It never returns.
ARGUMENTS
$message- the message key.\%args- its placeholders.
API SPECIFICATION
INPUT
{
message => { type => 'string' },
args => { type => 'hashref' },
}
OUTPUT
{ type => 'undef' }
MESSAGES
The message it is given.
COMMON PITFALLS
Untie the hash when you are done. The context holds the hash, the hash holds the resolver, and the resolver usually holds the context. That loop is only broken by
untie. Drop the objecttiereturned first, oruntiewarns that a reference still exists.The hash is read-only. Storing or deleting a key dies, because the class has no
STOREorDELETE.Once a read fails, later failures report the first one. This is what makes a chain report its cause, not its last link. Use a new tie for a new set of variables.
ENCODING
Names and values are Perl character strings and may hold any text, including non-ASCII characters and emoji. Nothing is encoded or decoded here.
AUTHOR
Nigel Horne, njh@nigelhorne.com
FORMAL SPECIFICATION
This section describes each routine in the Z notation, a precise mathematical language for describing programs. You do not need to read it to use the module: everything here is also said in plain words in the sections above.
How to read it: a box names a routine. The lines above the middle rule declare the values it works with. A name ending in ? is an input, and a name ending in ! is an output. ΔEnv means the routine may change the state; ΞEnv means it does not. The lines below the rule are always true when the routine returns.
Env ≙ [ raw : NAME ⇸ TEXT; resolved : NAME ⇸ TEXT;
busy : ℙ NAME; failure : optional ERROR;
resolve : NAME → TEXT ]
TIEHASH
┌─ TieHash ─────────────────────────────────────────
│ Env'
│ raw? : NAME ⇸ TEXT; resolve? : NAME → TEXT
├───────────────────────────────────────────────────
│ raw' = raw? ∧ resolve' = resolve?
│ resolved' = ∅ ∧ busy' = ∅ ∧ failure' = ⊥
└───────────────────────────────────────────────────
FETCH
┌─ Fetch ───────────────────────────────────────────
│ ΔEnv
│ key? : NAME
│ v! : optional TEXT
├───────────────────────────────────────────────────
│ key? ∈ dom resolved ⇒ v! = resolved(key?) ∧ θEnv' = θEnv
│ key? ¬in; dom raw ⇒ v! = ⊥ ∧ θEnv' = θEnv
│ key? ∈ busy ⇒ raises Cycle(key?) ∧ failure' = failure ∨ Cycle(key?)
│ otherwise, with busy ∪ {key?} while resolve(key?) runs:
│ resolve(key?) = t ⇒ v! = t ∧ resolved' = resolved ⊕ {key? ↦ t}
│ resolve(key?) fails ∧ failure ≠ ⊥ ⇒ raises failure
│ resolve(key?) fails ∧ failure = ⊥ ⇒ raises Failed(key?)
│ ∧ failure' = Failed(key?)
│ busy' = busy
└───────────────────────────────────────────────────
EXISTS, FIRSTKEY, NEXTKEY and SCALAR
┌─ Keys ────────────────────────────────────────────
│ ΞEnv
├───────────────────────────────────────────────────
│ exists(k) ⇔ k ∈ dom raw
│ keys = dom raw
└───────────────────────────────────────────────────
failure
┌─ Failure ─────────────────────────────────────────
│ ΞEnv
│ r! : optional ERROR
├───────────────────────────────────────────────────
│ r! = failure
└───────────────────────────────────────────────────
fail
┌─ Fail ────────────────────────────────────────────
│ ΔEnv
│ message? : KEY; args? : NAME ⇸ TEXT
├───────────────────────────────────────────────────
│ failure' = Error(message?, args?)
│ raises failure'
└───────────────────────────────────────────────────
STATE DIAGRAM
Each variable is in one of three states. A read of the variable moves it:
$env{A}
+----------------+ (first read) +-----------------+
| RAW |-------------->| BUSY |
| text only | | resolve('A') |
+----------------+ | is running |
+-----------------+
| | |
resolve returned t | | | $env{A} read again
v | v (a cycle)
+-----------------+ | +------------------------+
| RESOLVED | | | fail: runner.env_cycle |
| resolved{A}=t | | | failure := the error |
+-----------------+ | +------------------------+
| |
later reads | | resolve died
return t at once | v
v +---------------------------------+
$env{A} | failure set? raise it again |
| else fail: runner.env_failed |
+---------------------------------+
After a failure, A goes back to RAW (BUSY is undone on the way out),
and failure keeps the first error until the hash is untied.
LICENSE AND COPYRIGHT
Copyright 2026 Nigel Horne.
This program is released under the GNU General Public License, version 2. See the LICENSE file that comes with this distribution. If you use it, please let me know.