88
99use regex:: Regex ;
1010use std:: path:: PathBuf ;
11+ use std:: sync:: LazyLock ;
1112use thiserror:: Error ;
1213
14+ /// Static regex for environment variable substitution: {env:VAR} or {env:VAR:default}
15+ /// Group 1: variable name
16+ /// Group 2: optional default value (after second colon)
17+ static ENV_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
18+ Regex :: new ( r"\{env:([^:}]+)(?::([^}]*))?\}" )
19+ . expect ( "env regex pattern is valid and tested" )
20+ } ) ;
21+
22+ /// Static regex for file content substitution: {file:path}
23+ /// Group 1: file path
24+ static FILE_REGEX : LazyLock < Regex > = LazyLock :: new ( || {
25+ Regex :: new ( r"\{file:([^}]+)\}" ) . expect ( "file regex pattern is valid and tested" )
26+ } ) ;
27+
1328/// Errors that can occur during configuration substitution.
1429#[ derive( Debug , Error ) ]
1530pub enum SubstitutionError {
@@ -42,11 +57,13 @@ pub enum SubstitutionError {
4257///
4358/// Handles replacement of `{env:...}` and `{file:...}` placeholders
4459/// in configuration strings.
60+ ///
61+ /// This struct uses statically initialized regex patterns via `LazyLock`,
62+ /// making regex compilation a one-time cost shared across all instances.
4563pub struct ConfigSubstitution {
46- /// Regex for environment variable substitution: {env:VAR} or {env:VAR:default}
47- env_regex : Regex ,
48- /// Regex for file content substitution: {file:path}
49- file_regex : Regex ,
64+ // This struct is kept for API compatibility.
65+ // Regex patterns are now static module-level constants.
66+ _private : ( ) ,
5067}
5168
5269impl Default for ConfigSubstitution {
@@ -56,22 +73,13 @@ impl Default for ConfigSubstitution {
5673}
5774
5875impl ConfigSubstitution {
59- /// Creates a new `ConfigSubstitution` instance with compiled regex patterns.
76+ /// Creates a new `ConfigSubstitution` instance.
77+ ///
78+ /// The regex patterns are statically initialized on first use,
79+ /// so creating multiple instances has no additional cost.
6080 #[ must_use]
6181 pub fn new ( ) -> Self {
62- Self {
63- // Matches {env:VAR_NAME} or {env:VAR_NAME:default_value}
64- // Group 1: variable name
65- // Group 2: optional default value (after second colon)
66- env_regex : Regex :: new ( r"\{env:([^:}]+)(?::([^}]*))?\}" ) . unwrap_or_else ( |e| {
67- panic ! ( "Failed to compile env regex: {e}" ) ;
68- } ) ,
69- // Matches {file:path}
70- // Group 1: file path
71- file_regex : Regex :: new ( r"\{file:([^}]+)\}" ) . unwrap_or_else ( |e| {
72- panic ! ( "Failed to compile file regex: {e}" ) ;
73- } ) ,
74- }
82+ Self { _private : ( ) }
7583 }
7684
7785 /// Substitutes all variables in a string.
@@ -109,8 +117,7 @@ impl ConfigSubstitution {
109117 let mut error: Option < SubstitutionError > = None ;
110118
111119 // Collect all matches first to avoid borrowing issues
112- let matches: Vec < _ > = self
113- . env_regex
120+ let matches: Vec < _ > = ENV_REGEX
114121 . captures_iter ( input)
115122 . map ( |cap| {
116123 let full_match = cap. get ( 0 ) . map ( |m| m. as_str ( ) . to_string ( ) ) ;
@@ -155,8 +162,7 @@ impl ConfigSubstitution {
155162 let mut error: Option < SubstitutionError > = None ;
156163
157164 // Collect all matches first
158- let matches: Vec < _ > = self
159- . file_regex
165+ let matches: Vec < _ > = FILE_REGEX
160166 . captures_iter ( input)
161167 . map ( |cap| {
162168 let full_match = cap. get ( 0 ) . map ( |m| m. as_str ( ) . to_string ( ) ) ;
0 commit comments