roster/src/variable_substitution.py

245 lines
9.1 KiB
Python

# variable_substitution.py
#
# Copyright 2025 Pavel Baksy
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
# SPDX-License-Identifier: GPL-3.0-or-later
import re
from typing import Dict, List, Tuple, Set
from .models import HttpRequest, Environment
class VariableSubstitution:
"""Handles variable substitution with {{variable_name}} syntax."""
# Pattern to match {{variable_name}} where variable_name is alphanumeric + underscore
VARIABLE_PATTERN = re.compile(r'\{\{(\w+)\}\}')
@staticmethod
def find_variables(text: str) -> List[str]:
"""
Extract all {{variable_name}} references from text.
Args:
text: The text to search for variable references
Returns:
List of variable names found (without the {{ }} delimiters)
"""
if not text:
return []
return VariableSubstitution.VARIABLE_PATTERN.findall(text)
@staticmethod
def substitute(text: str, variables: Dict[str, str]) -> Tuple[str, List[str]]:
"""
Replace {{variable_name}} with values from variables dict.
Args:
text: The text containing variable placeholders
variables: Dictionary mapping variable names to their values
Returns:
Tuple of (substituted_text, list_of_undefined_variables)
"""
if not text:
return text, []
undefined_vars = []
def replace_var(match):
var_name = match.group(1)
if var_name in variables:
value = variables[var_name]
# Check if value is empty/None - treat as undefined for warning purposes
if not value:
undefined_vars.append(var_name)
return ""
return value
else:
# Variable not defined - track it and replace with empty string
undefined_vars.append(var_name)
return ""
substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text)
return substituted, undefined_vars
@staticmethod
def substitute_request(request: HttpRequest, environment: Environment) -> Tuple[HttpRequest, Set[str]]:
"""
Substitute variables in all request fields (URL, headers, body).
Args:
request: The HTTP request with variable placeholders
environment: The environment containing variable values
Returns:
Tuple of (new_request_with_substitutions, set_of_undefined_variables)
"""
all_undefined = set()
# Substitute URL
new_url, url_undefined = VariableSubstitution.substitute(
request.url, environment.variables
)
all_undefined.update(url_undefined)
# Substitute headers (both keys and values)
new_headers = {}
for key, value in request.headers.items():
new_key, key_undefined = VariableSubstitution.substitute(
key, environment.variables
)
new_value, value_undefined = VariableSubstitution.substitute(
value, environment.variables
)
all_undefined.update(key_undefined)
all_undefined.update(value_undefined)
new_headers[new_key] = new_value
# Substitute body
new_body, body_undefined = VariableSubstitution.substitute(
request.body, environment.variables
)
all_undefined.update(body_undefined)
# Create new HttpRequest with substituted values
new_request = HttpRequest(
method=request.method,
url=new_url,
headers=new_headers,
body=new_body,
syntax=request.syntax
)
return new_request, all_undefined
@staticmethod
def substitute_excluding_variables(text: str, variables: Dict[str, str], excluded_vars: List[str]) -> Tuple[str, List[str]]:
"""
Replace {{variable_name}} with values, but skip excluded variables (leave as {{var}}).
Args:
text: The text containing variable placeholders
variables: Dictionary mapping variable names to their values
excluded_vars: List of variable names to skip (leave as placeholders)
Returns:
Tuple of (substituted_text, list_of_undefined_variables)
"""
if not text:
return text, []
undefined_vars = []
def replace_var(match):
var_name = match.group(1)
# Skip excluded variables - leave as {{var_name}}
if var_name in excluded_vars:
return match.group(0) # Return original {{var_name}}
# Substitute non-excluded variables
if var_name in variables:
value = variables[var_name]
# Check if value is empty/None - treat as undefined for warning purposes
if not value:
undefined_vars.append(var_name)
return ""
return value
else:
# Variable not defined - track it and replace with empty string
undefined_vars.append(var_name)
return ""
substituted = VariableSubstitution.VARIABLE_PATTERN.sub(replace_var, text)
return substituted, undefined_vars
@staticmethod
def substitute_request_excluding_sensitive(
request: HttpRequest,
environment: Environment,
sensitive_variables: List[str]
) -> Tuple[HttpRequest, Set[str], bool]:
"""
Substitute variables in request, but exclude sensitive variables (leave as {{var}}).
This is useful for saving requests to history without exposing sensitive values.
Sensitive variables remain in their {{variable_name}} placeholder form.
Args:
request: The HTTP request with variable placeholders
environment: The environment containing variable values
sensitive_variables: List of variable names to exclude from substitution
Returns:
Tuple of (redacted_request, set_of_undefined_variables, has_sensitive_vars)
- redacted_request: Request with only non-sensitive variables substituted
- set_of_undefined_variables: Variables that were referenced but not defined
- has_sensitive_vars: True if any sensitive variables were found and redacted
"""
all_undefined = set()
# Substitute URL (excluding sensitive variables)
new_url, url_undefined = VariableSubstitution.substitute_excluding_variables(
request.url, environment.variables, sensitive_variables
)
all_undefined.update(url_undefined)
# Substitute headers (both keys and values, excluding sensitive variables)
new_headers = {}
for key, value in request.headers.items():
new_key, key_undefined = VariableSubstitution.substitute_excluding_variables(
key, environment.variables, sensitive_variables
)
new_value, value_undefined = VariableSubstitution.substitute_excluding_variables(
value, environment.variables, sensitive_variables
)
all_undefined.update(key_undefined)
all_undefined.update(value_undefined)
new_headers[new_key] = new_value
# Substitute body (excluding sensitive variables)
new_body, body_undefined = VariableSubstitution.substitute_excluding_variables(
request.body, environment.variables, sensitive_variables
)
all_undefined.update(body_undefined)
# Create new HttpRequest with redacted values
redacted_request = HttpRequest(
method=request.method,
url=new_url,
headers=new_headers,
body=new_body,
syntax=request.syntax
)
# Check if any sensitive variables were actually used in the request
all_vars_in_request = set()
all_vars_in_request.update(VariableSubstitution.find_variables(request.url))
all_vars_in_request.update(VariableSubstitution.find_variables(request.body))
for key, value in request.headers.items():
all_vars_in_request.update(VariableSubstitution.find_variables(key))
all_vars_in_request.update(VariableSubstitution.find_variables(value))
# Determine if any sensitive variables were actually present
has_sensitive_vars = bool(
set(sensitive_variables) & all_vars_in_request
)
return redacted_request, all_undefined, has_sensitive_vars