Krikri::VariableSubstitutor::CrinjaRenderer
CrinjaRenderer - Handles full Jinja2 template rendering using Crinja This includes {% if %}, {% for %}, {% set %}, etc.
Constants
Real bug found benchmarking geerlingguy.postgresql: its own
pg_hba.conf.j2 iterates postgresql_hba_entries (a list of
dicts) via {% for client in ... %} ... {{ client.auth_method }} ...{% endfor %}, where each entry's auth_method: field is
itself "{{ postgresql_auth_method }}" - a role default
computed from ANOTHER default, the same recursive-re-templating
shape this codebase has already fixed a dozen-odd times over
for plain scalar variable values. This is a distinct sub-case
none of those fixes covered: only a top-level String value
used to get re-rendered - postgresql_hba_entries itself is an
Array, so it never even reached the raw.is_a?(String) check at
all, and the literal unrendered {{ postgresql_auth_method }}
text landed straight into the rendered config file (PostgreSQL
then refused to start: "invalid authentication method '{{'").
Real Ansible's own recursive re-templating applies at every
level of a nested structure, not just the outermost value -
walks Array/Hash values recursively, re-rendering every String
leaf that still contains "{{".
Exposed as a class method for the same reason
#json_any_to_crinja_value is: TemplateActionPlugin has its own
separate prepare_*_vars (a genuinely separate Crinja
environment - see that method's own comment) that needs this
identical recursive-re-render fix, not just this class's.
Narrow special case for one specific idiom (found round 755/753,
jtyr.nsswitch/jtyr.motd): some_var: "{{ some_dict.update( other_dict) }}{{ some_dict }}" - call .update() purely for its
mutating side effect, discard its None return, then render the
now-merged dict. Real Ansible's templar preserves the result as
a genuine dict (_AnsibleLazyTemplateDict, private ansible-core
internals - see KNOWN_MISSING.md's own writeup); replicating
that faithfully (deferred evaluation + type preservation through
the whole vars pipeline) is a major architectural undertaking,
not attempted here. This instead special-cases exactly the
documented shape - both operands bare variable names, no
arbitrary Jinja expression inside .update(...) - by reading
both from the vars store directly, merging (matching Python
dict.update's own shallow-merge, top-level-key-overrides
semantics), and persisting the merge back onto the target
variable (matching Python's real in-place mutation, visible to
any LATER reference of it too - not just this one). Falls
through to the general string-rendering path below for anything
that doesn't match this exact shape.
Constructors
Class methods
Converts the hostvars magic variable with each host's vars
dict wrapped in Krikri::HostVarsVarsDict (see that class's own
comment). Shared by BOTH Crinja context builds that can carry
hostvars - LazyCrinjaContext#convert (the {% %}/{{ }}
evaluator's lazy context, via #convert_var) and the template
module's eager env (template_action_plugin.cr) - so a raising
attribute miss behaves identically in a .j2 file and a
module-arg render.
Converts one @vars entry to its final Crinja::Value, applying
the same recursive re-templating #rerender_nested_templates
always did, bounded by the depth guard above. Called from
LazyCrinjaContext#convert - kept here (not on that class)
because it needs @@prepare_crinja_vars_depth, a CrinjaRenderer
class variable shared across every renderer/context in the
process, matching the guard's own "process-wide, not
per-instance" reasoning (see VarSubstitutor's identical
@@block_tag_escalation_depth comment).
Convert Crinja::Value to JSON::Any - the reverse direction of #json_any_to_crinja_value below. Exposed as a class method for the same reason that one is (shareable with any other Crinja environment this codebase spins up).
Real Ansible's omit inside a CONTAINER removes that entry
rather than leaving a placeholder in it - verified against
ansible-core 2.19.4: {{ [1, v_omit, 3] }} renders [1, 3] and
{{ {'a': 1, 'b': v_omit} }} renders {"a": 1}. Crinja builds
such a literal itself (this is the raw-value path every bracket/
dict expression takes), so it sees omit as the ordinary string
this engine represents it with, and kept it - the literal
sentinel text then landed in whatever the list/dict fed.
ExpressionEvaluator's own literal-array/dict builders need the
same treatment separately: the two evaluators share no
implementation, so this bug class has to be fixed once in each
(see CLAUDE.md). Only containers are touched here - a bare
scalar omit must survive intact this far, since that is what
tells the caller to drop a whole parameter.
Convert JSON::Any to Crinja::Value.
Exposed as a class method because TemplateActionPlugin needs the exact same coercion and used to carry a verbatim copy of it. (Only the converter is shared: that plugin's Crinja environment genuinely must stay separate, since its trim_blocks/lstrip_blocks come from the task's own template: params and therefore vary per task - unlike this class's, whose config is invariant and so can be one process-wide instance.)
True if name resolves in the shared environment's filter library - a registered filter or a registered alias for one (FeatureLibrary#[] downcases lookups and resolves aliases the same way, so this mirrors exactly what a render would find).
True if name resolves in the shared environment's TEST
library - the test-side twin of #known_filter? above, for
ConditionalEvaluator's compile-time test-name pre-pass (an
unknown is <name> in a when: must hard-fail even when
and/or short-circuiting never reaches that clause).
Parses Crinja's own unknown-feature error wording ("no filter/ test with name ... registered") into {kind, name} - shared by #render's rescue and #evaluate_value!'s rescue (the two Crinja entry points that can surface an unregistered filter/test at evaluation time), so both react to the same feature kinds the same way.
Instance methods
Evaluates expr (bare Jinja expression text, no surrounding
{{ }}) and returns its RAW structured result as JSON::Any
(nil for a genuinely undefined result - the same nilable
convention VariableLookup#resolve/#resolve_simple/etc.
already use) instead of #render!'s always-a-String output.
#render! goes through Template#render, which always produces
a String via Crinja::Finalizer#stringify - fine for a FINAL
{{ }} substitution, but wrong for a caller (like
ExpressionEvaluator's Crinja-delegation branches) that needs to
hand the result to something else expecting structured data
(another filter, a .get() call, a nested nested expression) or
that wants to format an Array/Hash result through THIS
codebase's own VariableLookup#format_value (its JSON-compact
style, not Crinja's Python-repr Finalizer style) so the
existing internal "render sub-expression to a String, JSON. parse it back into structured data" round trip used throughout
expression_evaluator.cr/filter_engine.cr/comparison_ evaluator.cr/variable_lookup.cr keeps working unchanged - the
general filter-chain dispatch investigation found this was
necessary (a naive
format_value Python-repr rewrite broke that round trip outright
since Python-repr text isn't valid JSON).
Parses via Crinja::Parser::ExpressionLexer/ExpressionParser
directly (bypassing Template/from_string entirely - there is
no template TAG here, just a bare expression) and
Crinja::Environment#evaluate(ast_node, bindings) : Value
(lib/crinja/src/environment.cr:106-108), the overload that
returns the raw Value rather than a stringified result -
mirrors the fork's own spec_helper.cr#evaluate_expression_raw
test helper, which uses the identical parse-then-evaluate
sequence for the same reason (getting at the raw value, not
Crinja's own stringified rendering of it).
Render a template containing Jinja2 control structures, raising
on any failure instead of swallowing it - for a caller (like
ExpressionEvaluator's own Crinja-delegation branches) that
wants to fall back to a DIFFERENT
rendering strategy on failure, rather than #render's own
"give back the original unrendered text" behavior, which would
be actively wrong for a caller expecting a real evaluated value.