@@ -284,6 +284,10 @@ pub fn render_properties(
284284
285285/// Build the full context for a resource by merging global context with resource properties.
286286/// Matches Python's `get_full_context`.
287+ ///
288+ /// Injects `resource_name` as a special variable (like `stack_name` and `stack_env`)
289+ /// containing the current resource's name. Any global values that contain deferred
290+ /// template expressions (e.g., `{{ resource_name }}`) are re-rendered at this point.
287291pub fn get_full_context (
288292 engine : & TemplateEngine ,
289293 global_context : & HashMap < String , String > ,
@@ -292,9 +296,19 @@ pub fn get_full_context(
292296) -> HashMap < String , String > {
293297 debug ! ( "Getting full context for {}..." , resource. name) ;
294298
295- let prop_context = render_properties ( engine, & resource. props , global_context, stack_env) ;
299+ // Inject resource_name into the context so it's available in props and re-rendered globals
300+ let mut context_with_resource_name = global_context. clone ( ) ;
301+ context_with_resource_name. insert ( "resource_name" . to_string ( ) , resource. name . clone ( ) ) ;
296302
297- let mut full_context = global_context. clone ( ) ;
303+ // Re-render any global values that contain deferred template expressions.
304+ // This allows globals (e.g., global_tags) to use {{ resource_name }} which couldn't
305+ // be resolved at global rendering time since the resource wasn't known yet.
306+ let resolved_context =
307+ re_render_context_with_deferred_vars ( engine, & context_with_resource_name) ;
308+
309+ let prop_context = render_properties ( engine, & resource. props , & resolved_context, stack_env) ;
310+
311+ let mut full_context = resolved_context;
298312 for ( k, v) in prop_context {
299313 full_context. insert ( k, v) ;
300314 }
@@ -303,6 +317,39 @@ pub fn get_full_context(
303317 full_context
304318}
305319
320+ /// Re-render context values that contain deferred template expressions (`{{ ... }}`).
321+ /// This is used to resolve variables like `resource_name` that weren't available
322+ /// when globals were initially rendered.
323+ fn re_render_context_with_deferred_vars (
324+ engine : & TemplateEngine ,
325+ context : & HashMap < String , String > ,
326+ ) -> HashMap < String , String > {
327+ let mut result = context. clone ( ) ;
328+
329+ for ( key, value) in context {
330+ if value. contains ( "{{" ) {
331+ match engine. render ( value, context) {
332+ Ok ( rendered) => {
333+ let rendered = rendered. replace ( "True" , "true" ) . replace ( "False" , "false" ) ;
334+ debug ! (
335+ "Re-rendered deferred global [{}]: {} -> {}" ,
336+ key, value, rendered
337+ ) ;
338+ result. insert ( key. clone ( ) , rendered) ;
339+ }
340+ Err ( e) => {
341+ debug ! (
342+ "Warning: could not re-render deferred global '{}': {}" ,
343+ key, e
344+ ) ;
345+ }
346+ }
347+ }
348+ }
349+
350+ result
351+ }
352+
306353/// Prepare context for SQL query rendering.
307354/// JSON string values are re-serialized to ensure proper format (compact, lowercase bools).
308355/// Matches Python's `render_queries` context preparation.
@@ -351,3 +398,152 @@ pub fn is_json(s: &str) -> bool {
351398 Err ( _) => false ,
352399 }
353400}
401+
402+ #[ cfg( test) ]
403+ mod tests {
404+ use super :: * ;
405+ use crate :: resource:: manifest:: { Property , Resource } ;
406+
407+ /// Helper to create a minimal Resource for testing.
408+ fn make_resource ( name : & str , props : Vec < Property > ) -> Resource {
409+ Resource {
410+ name : name. to_string ( ) ,
411+ r#type : "resource" . to_string ( ) ,
412+ file : None ,
413+ sql : None ,
414+ run : None ,
415+ props,
416+ exports : vec ! [ ] ,
417+ protected : vec ! [ ] ,
418+ description : String :: new ( ) ,
419+ r#if : None ,
420+ skip_validation : None ,
421+ auth : None ,
422+ }
423+ }
424+
425+ /// Helper to create a Property with a simple string value.
426+ fn make_prop ( name : & str , value : & str ) -> Property {
427+ Property {
428+ name : name. to_string ( ) ,
429+ value : Some ( serde_yaml:: Value :: String ( value. to_string ( ) ) ) ,
430+ values : None ,
431+ description : String :: new ( ) ,
432+ merge : None ,
433+ }
434+ }
435+
436+ #[ test]
437+ fn test_resource_name_available_in_full_context ( ) {
438+ let engine = TemplateEngine :: new ( ) ;
439+ let mut global_context = HashMap :: new ( ) ;
440+ global_context. insert ( "stack_name" . to_string ( ) , "my-stack" . to_string ( ) ) ;
441+ global_context. insert ( "stack_env" . to_string ( ) , "dev" . to_string ( ) ) ;
442+
443+ let resource = make_resource ( "cross_account_role" , vec ! [ ] ) ;
444+
445+ let ctx = get_full_context ( & engine, & global_context, & resource, "dev" ) ;
446+
447+ assert_eq ! ( ctx. get( "resource_name" ) . unwrap( ) , "cross_account_role" ) ;
448+ // Existing variables still present
449+ assert_eq ! ( ctx. get( "stack_name" ) . unwrap( ) , "my-stack" ) ;
450+ assert_eq ! ( ctx. get( "stack_env" ) . unwrap( ) , "dev" ) ;
451+ }
452+
453+ #[ test]
454+ fn test_resource_name_usable_in_props ( ) {
455+ let engine = TemplateEngine :: new ( ) ;
456+ let mut global_context = HashMap :: new ( ) ;
457+ global_context. insert ( "stack_name" . to_string ( ) , "my-stack" . to_string ( ) ) ;
458+ global_context. insert ( "stack_env" . to_string ( ) , "dev" . to_string ( ) ) ;
459+
460+ let resource = make_resource (
461+ "cross_account_role" ,
462+ vec ! [ make_prop( "tag_value" , "{{ resource_name }}" ) ] ,
463+ ) ;
464+
465+ let ctx = get_full_context ( & engine, & global_context, & resource, "dev" ) ;
466+
467+ assert_eq ! ( ctx. get( "tag_value" ) . unwrap( ) , "cross_account_role" ) ;
468+ }
469+
470+ #[ test]
471+ fn test_resource_name_resolves_in_deferred_globals ( ) {
472+ let engine = TemplateEngine :: new ( ) ;
473+ let mut global_context = HashMap :: new ( ) ;
474+ global_context. insert ( "stack_name" . to_string ( ) , "my-stack" . to_string ( ) ) ;
475+ global_context. insert ( "stack_env" . to_string ( ) , "dev" . to_string ( ) ) ;
476+ // Simulate a global that was rendered at startup but contained {{ resource_name }}
477+ // which couldn't be resolved then, so it's preserved as a literal.
478+ global_context. insert (
479+ "global_tags" . to_string ( ) ,
480+ r#"[{"Key":"stackql:resource-name","Value":"{{ resource_name }}"}]"# . to_string ( ) ,
481+ ) ;
482+
483+ let resource = make_resource ( "cross_account_role" , vec ! [ ] ) ;
484+
485+ let ctx = get_full_context ( & engine, & global_context, & resource, "dev" ) ;
486+
487+ let global_tags = ctx. get ( "global_tags" ) . unwrap ( ) ;
488+ assert ! (
489+ global_tags. contains( "cross_account_role" ) ,
490+ "global_tags should contain the resolved resource name, got: {}" ,
491+ global_tags
492+ ) ;
493+ assert ! (
494+ !global_tags. contains( "{{ resource_name }}" ) ,
495+ "global_tags should not contain unresolved template expression"
496+ ) ;
497+ }
498+
499+ #[ test]
500+ fn test_resource_name_varies_per_resource ( ) {
501+ let engine = TemplateEngine :: new ( ) ;
502+ let mut global_context = HashMap :: new ( ) ;
503+ global_context. insert ( "stack_name" . to_string ( ) , "my-stack" . to_string ( ) ) ;
504+ global_context. insert ( "stack_env" . to_string ( ) , "dev" . to_string ( ) ) ;
505+ global_context. insert (
506+ "global_tags" . to_string ( ) ,
507+ r#"[{"Key":"res","Value":"{{ resource_name }}"}]"# . to_string ( ) ,
508+ ) ;
509+
510+ let res1 = make_resource ( "vpc_network" , vec ! [ ] ) ;
511+ let res2 = make_resource ( "storage_bucket" , vec ! [ ] ) ;
512+
513+ let ctx1 = get_full_context ( & engine, & global_context, & res1, "dev" ) ;
514+ let ctx2 = get_full_context ( & engine, & global_context, & res2, "dev" ) ;
515+
516+ assert_eq ! ( ctx1. get( "resource_name" ) . unwrap( ) , "vpc_network" ) ;
517+ assert_eq ! ( ctx2. get( "resource_name" ) . unwrap( ) , "storage_bucket" ) ;
518+ assert ! ( ctx1. get( "global_tags" ) . unwrap( ) . contains( "vpc_network" ) ) ;
519+ assert ! ( ctx2. get( "global_tags" ) . unwrap( ) . contains( "storage_bucket" ) ) ;
520+ }
521+
522+ #[ test]
523+ fn test_re_render_context_no_templates_is_noop ( ) {
524+ let engine = TemplateEngine :: new ( ) ;
525+ let mut context = HashMap :: new ( ) ;
526+ context. insert ( "stack_name" . to_string ( ) , "my-stack" . to_string ( ) ) ;
527+ context. insert ( "plain_value" . to_string ( ) , "no templates here" . to_string ( ) ) ;
528+
529+ let result = re_render_context_with_deferred_vars ( & engine, & context) ;
530+
531+ assert_eq ! ( result. get( "stack_name" ) . unwrap( ) , "my-stack" ) ;
532+ assert_eq ! ( result. get( "plain_value" ) . unwrap( ) , "no templates here" ) ;
533+ }
534+
535+ #[ test]
536+ fn test_re_render_context_resolves_deferred_vars ( ) {
537+ let engine = TemplateEngine :: new ( ) ;
538+ let mut context = HashMap :: new ( ) ;
539+ context. insert ( "resource_name" . to_string ( ) , "my_resource" . to_string ( ) ) ;
540+ context. insert (
541+ "tag" . to_string ( ) ,
542+ "resource:{{ resource_name }}" . to_string ( ) ,
543+ ) ;
544+
545+ let result = re_render_context_with_deferred_vars ( & engine, & context) ;
546+
547+ assert_eq ! ( result. get( "tag" ) . unwrap( ) , "resource:my_resource" ) ;
548+ }
549+ }
0 commit comments