@@ -6,6 +6,7 @@ use crate::config_migrate::migration::{
66use crate :: sub_agent:: effective_agents_assembler:: AgentTypeDefinitionError ;
77use fs:: LocalFile ;
88use fs:: file_reader:: { FileReader , FileReaderError } ;
9+ use regex:: Regex ;
910use std:: collections:: { HashMap , HashSet } ;
1011use std:: path:: Path ;
1112use thiserror:: Error ;
@@ -125,20 +126,35 @@ fn retrieve_dir_mapping_values<F: FileReader>(
125126
126127 let read_files = read_files. try_fold ( HashMap :: new ( ) , |mut acc, read_file| {
127128 let ( filepath, content) = read_file?;
128- let parsed = serde_yaml:: from_str :: < serde_yaml:: Value > ( & content) ?;
129+ let parsed = serde_yaml:: from_str :: < serde_yaml:: Value > ( & process_config_input ( content) ) ?;
129130 acc. insert ( filepath, parsed) ;
130131 Ok :: < _ , ConversionError > ( acc)
131132 } ) ?;
132133
133134 Ok ( serde_yaml:: to_value ( read_files) ?)
134135}
135136
137+ /// Handles the usage of environment variables in the YAML config files via the special
138+ /// `{{VAR_NAME}}` syntax, by replacing them with a YAML-compatible syntax `'{{VAR_NAME}}'`.
139+ /// (just adding quotes to make it a string). If this pattern is not quoted, the resulting YAML
140+ /// would evaluate to a nested mapping with a single key-null pair, which is not what we want.
141+ ///
142+ /// This is a simple, non-performant approach that may not cover all edge cases, but works for
143+ /// the common scenarios we expect (small config strings).
144+ fn process_config_input ( input : String ) -> String {
145+ // This regex matches {{VAR_NAME}} not already inside quotes
146+ let re = Regex :: new ( r#"(?P<pre>[^'"]|^)\{\{([A-Za-z0-9_]+)\}\}(?P<post>[^'"]|$)"# ) . unwrap ( ) ;
147+ re. replace_all ( & input, "${pre}'{{${2}}}'${post}" )
148+ . to_string ( )
149+ }
150+
136151#[ cfg( test) ]
137152mod tests {
138153 use std:: path:: { Path , PathBuf } ;
139154
140155 use fs:: mock:: MockLocalFile ;
141156 use mockall:: { PredicateBooleanExt , predicate} ;
157+ use rstest:: rstest;
142158
143159 use crate :: config_migrate:: migration:: config:: DirInfo ;
144160
@@ -189,6 +205,27 @@ logs:
189205 pattern: WARN|ERROR
190206"# ;
191207
208+ #[ rstest]
209+ #[ case:: no_templates( "license_key: {{MY_ENV_VAR}}" , "license_key: '{{MY_ENV_VAR}}'" ) ]
210+ #[ case:: multiple_templates(
211+ "license_key: {{MY_ENV_VAR}} other {{ANOTHER_ENV}}" ,
212+ "license_key: '{{MY_ENV_VAR}}' other '{{ANOTHER_ENV}}'"
213+ ) ]
214+ #[ case:: no_templates_at_all(
215+ "license_key: my_real_license_key" ,
216+ "license_key: my_real_license_key"
217+ ) ]
218+ #[ case:: multiline_yaml_syntax(
219+ "license_key: {{MY_ENV_VAR}}\n other_key: value" ,
220+ "license_key: '{{MY_ENV_VAR}}'\n other_key: value"
221+ ) ]
222+ #[ case:: already_quoted( "license_key: '{{MY_ENV_VAR}}'" , "license_key: '{{MY_ENV_VAR}}'" ) ]
223+ #[ case:: double_quoted( r#"license_key: "{{MY_ENV_VAR}}""# , "license_key: \" {{MY_ENV_VAR}}\" " ) ]
224+ fn env_var_interpolation ( #[ case] input : & str , #[ case] output : & str ) {
225+ let result = process_config_input ( input. to_string ( ) ) ;
226+ assert_eq ! ( result, output) ;
227+ }
228+
192229 #[ test]
193230 fn from_migration_config_to_conversion ( ) {
194231 // Sample config
0 commit comments