diff --git a/README.md b/README.md index d772922..0701c2f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ A modern HTTP client for GNOME, built with GTK 4 and libadwaita. - Configure custom headers and request bodies - View response headers and bodies - Track request history with persistence +- **JavaScript preprocessing and postprocessing scripts** +- **Project environments with variables** - Beautiful GNOME-native UI ## Dependencies @@ -16,6 +18,7 @@ A modern HTTP client for GNOME, built with GTK 4 and libadwaita. - libadwaita 1 - Python 3 - libsoup3 (provided by GNOME Platform) +- gjs (GNOME JavaScript) - for script execution ## Building @@ -30,3 +33,160 @@ sudo meson install -C builddir Roster uses libsoup3 (from GNOME Platform) for making HTTP requests - no external dependencies required. Run Roster from your application menu or with the `roster` command. + +## Scripts + +Roster supports JavaScript preprocessing and postprocessing scripts to automate request modifications and response data extraction. + +### Preprocessing Scripts + +**Run BEFORE the HTTP request is sent.** Use preprocessing to: +- Modify request headers, URL, body, or method +- Add dynamic values (timestamps, request IDs, signatures) +- Read environment variables +- Set/update environment variables + +**Available API:** +```javascript +// Request object (modifiable) +request.method // "GET", "POST", "PUT", "DELETE" +request.url // Full URL string +request.headers // Object with header key-value pairs +request.body // Request body string + +// Roster API +roster.getVariable(name) // Get variable from selected environment +roster.setVariable(name, value) // Set/update variable +roster.setVariables({key: value}) // Batch set variables +roster.project.name // Current project name +roster.project.environments // Array of environment names + +// Console output +console.log(message) // Output shown in preprocessing results +``` + +**Example 1: Add Dynamic Authentication Header** +```javascript +const token = roster.getVariable('auth_token'); +request.headers['Authorization'] = 'Bearer ' + token; +request.headers['X-Request-Time'] = new Date().toISOString(); +console.log('Added auth header for token:', token); +``` + +**Example 2: Modify Request Based on Environment** +```javascript +const env = roster.getVariable('environment_name'); +if (env === 'production') { + request.url = request.url.replace('localhost', 'api.example.com'); + console.log('Switched to production URL'); +} +``` + +**Example 3: Generate Request Signature** +```javascript +const apiKey = roster.getVariable('api_key'); +const timestamp = Date.now().toString(); +const requestId = Math.random().toString(36).substring(7); + +request.headers['X-API-Key'] = apiKey; +request.headers['X-Timestamp'] = timestamp; +request.headers['X-Request-ID'] = requestId; + +// Save for later reference +roster.setVariable('last_request_id', requestId); +console.log('Request ID:', requestId); +``` + +### Postprocessing Scripts + +**Run AFTER receiving the HTTP response.** Use postprocessing to: +- Extract data from response body +- Parse JSON/XML responses +- Store values in environment variables for use in subsequent requests +- Validate response data + +**Available API:** +```javascript +// Response object (read-only) +response.body // Response body as string +response.headers // Object with header key-value pairs +response.statusCode // HTTP status code (e.g., 200, 404) +response.statusText // Status text (e.g., "OK", "Not Found") +response.responseTime // Response time in milliseconds + +// Roster API +roster.setVariable(name, value) // Set/update variable +roster.setVariables({key: value}) // Batch set variables + +// Console output +console.log(message) // Output shown in script results +``` + +**Example 1: Extract Authentication Token** +```javascript +const data = JSON.parse(response.body); +if (data.access_token) { + roster.setVariable('auth_token', data.access_token); + console.log('Saved auth token'); +} +``` + +**Example 2: Extract Multiple Values** +```javascript +const data = JSON.parse(response.body); +roster.setVariables({ + user_id: data.user.id, + user_name: data.user.name, + session_id: data.session.id +}); +console.log('Extracted user:', data.user.name); +``` + +**Example 3: Validate and Store Response** +```javascript +const data = JSON.parse(response.body); +if (response.statusCode === 200 && data.items) { + roster.setVariable('item_count', data.items.length.toString()); + if (data.items.length > 0) { + roster.setVariable('first_item_id', data.items[0].id); + } + console.log('Found', data.items.length, 'items'); +} else { + console.log('Error: Invalid response'); +} +``` + +### Workflow Example: OAuth Token Flow + +**Request 1 (Login) - Postprocessing:** +```javascript +// Extract and store tokens from login response +const data = JSON.parse(response.body); +roster.setVariables({ + access_token: data.access_token, + refresh_token: data.refresh_token, + user_id: data.user_id +}); +console.log('Logged in as user:', data.user_id); +``` + +**Request 2 (API Call) - Preprocessing:** +```javascript +// Use stored token in subsequent request +const token = roster.getVariable('access_token'); +const userId = roster.getVariable('user_id'); + +request.headers['Authorization'] = 'Bearer ' + token; +request.url = request.url.replace('{userId}', userId); +console.log('Making authenticated request for user:', userId); +``` + +### Environment Variables + +Variables can be: +- Manually defined in "Manage Environments" +- Automatically created by scripts using `roster.setVariable()` +- Used in requests with `{{variable_name}}` syntax +- Read in preprocessing with `roster.getVariable()` + +Variables are scoped to environments within projects, allowing different values for development, staging, and production. diff --git a/src/request_tab_widget.py b/src/request_tab_widget.py index e04f96c..4068fd9 100644 --- a/src/request_tab_widget.py +++ b/src/request_tab_widget.py @@ -208,6 +208,91 @@ class RequestTabWidget(Gtk.Box): body_scroll.set_child(self.response_body_sourceview) self.response_stack.add_titled(body_scroll, "body", "Body") + # Preprocessing Results Panel (collapsible) + self.preprocessing_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) + self.preprocessing_results_container.set_visible(False) # Initially hidden + + # Separator + preprocessing_separator = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + preprocessing_separator.set_margin_top(6) + self.preprocessing_results_container.append(preprocessing_separator) + + # Header + preprocessing_header = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + preprocessing_header.set_margin_start(12) + preprocessing_header.set_margin_end(12) + preprocessing_header.set_margin_top(6) + preprocessing_header.set_margin_bottom(6) + + preprocessing_label = Gtk.Label(label="Preprocessing Results") + preprocessing_label.add_css_class("heading") + preprocessing_label.set_halign(Gtk.Align.START) + preprocessing_header.append(preprocessing_label) + + # Spacer + preprocessing_spacer = Gtk.Box() + preprocessing_spacer.set_hexpand(True) + preprocessing_header.append(preprocessing_spacer) + + # Status icon + self.preprocessing_status_icon = Gtk.Image() + self.preprocessing_status_icon.set_from_icon_name("emblem-ok-symbolic") + preprocessing_header.append(self.preprocessing_status_icon) + + self.preprocessing_results_container.append(preprocessing_header) + + # Output text view (scrollable) + self.preprocessing_output_scroll = Gtk.ScrolledWindow() + self.preprocessing_output_scroll.set_min_content_height(60) + self.preprocessing_output_scroll.set_max_content_height(60) + self.preprocessing_output_scroll.set_margin_start(12) + self.preprocessing_output_scroll.set_margin_end(12) + self.preprocessing_output_scroll.set_margin_bottom(6) + + self.preprocessing_output_textview = Gtk.TextView() + self.preprocessing_output_textview.set_editable(False) + self.preprocessing_output_textview.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + self.preprocessing_output_textview.set_monospace(True) + self.preprocessing_output_textview.set_left_margin(6) + self.preprocessing_output_textview.set_right_margin(6) + self.preprocessing_output_textview.set_top_margin(6) + self.preprocessing_output_textview.set_bottom_margin(6) + + self.preprocessing_output_scroll.set_child(self.preprocessing_output_textview) + self.preprocessing_results_container.append(self.preprocessing_output_scroll) + + # Expander control (separator-based, shown when output > 3 lines) + self.preprocessing_expander = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) + self.preprocessing_expander.set_margin_start(12) + self.preprocessing_expander.set_margin_end(12) + self.preprocessing_expander.set_margin_bottom(6) + self.preprocessing_expander.set_visible(False) # Hidden by default + + preprocessing_sep1 = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + preprocessing_sep1.set_hexpand(True) + self.preprocessing_expander.append(preprocessing_sep1) + + self.preprocessing_expander_label = Gtk.Label(label="Show full output") + self.preprocessing_expander_label.add_css_class("dim-label") + self.preprocessing_expander_label.add_css_class("caption") + self.preprocessing_expander.append(self.preprocessing_expander_label) + + preprocessing_sep2 = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) + preprocessing_sep2.set_hexpand(True) + self.preprocessing_expander.append(preprocessing_sep2) + + # Add click gesture for expander + preprocessing_expander_gesture = Gtk.GestureClick() + preprocessing_expander_gesture.connect("released", self._on_preprocessing_expander_clicked) + self.preprocessing_expander.add_controller(preprocessing_expander_gesture) + + self.preprocessing_results_container.append(self.preprocessing_expander) + + # Track expansion state + self.preprocessing_output_expanded = False + + response_box.append(self.preprocessing_results_container) + # Script Results Panel (collapsible) self.script_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) self.script_results_container.set_visible(False) # Initially hidden @@ -425,27 +510,26 @@ class RequestTabWidget(Gtk.Box): paned.set_position(300) paned.set_vexpand(True) - # Preprocessing Section (disabled for now) + # Preprocessing Section preprocessing_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) preprocessing_section.set_margin_start(12) preprocessing_section.set_margin_end(12) preprocessing_section.set_margin_top(12) preprocessing_section.set_margin_bottom(12) - preprocessing_label = Gtk.Label(label="Preprocessing (Coming Soon)") + preprocessing_label = Gtk.Label(label="Preprocessing") preprocessing_label.set_halign(Gtk.Align.START) preprocessing_label.add_css_class("heading") - preprocessing_label.add_css_class("dim-label") preprocessing_section.append(preprocessing_label) preprocessing_scroll = Gtk.ScrolledWindow() preprocessing_scroll.set_vexpand(True) self.preprocessing_sourceview = GtkSource.View() - self.preprocessing_sourceview.set_editable(False) - self.preprocessing_sourceview.set_sensitive(False) + self.preprocessing_sourceview.set_editable(True) + self.preprocessing_sourceview.set_sensitive(True) self.preprocessing_sourceview.set_show_line_numbers(True) - self.preprocessing_sourceview.set_highlight_current_line(False) + self.preprocessing_sourceview.set_highlight_current_line(True) self.preprocessing_sourceview.set_left_margin(12) self.preprocessing_sourceview.set_right_margin(12) self.preprocessing_sourceview.set_top_margin(12) @@ -471,15 +555,29 @@ class RequestTabWidget(Gtk.Box): Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION ) + # Add placeholder text + preprocessing_buffer = self.preprocessing_sourceview.get_buffer() + placeholder_text = """// Available: request object (modifiable) +// - request.method: "GET", "POST", "PUT", "DELETE" +// - request.url: Full URL string +// - request.headers: Object with header key-value pairs +// - request.body: Request body string +// +// Available: roster API +// - roster.getVariable(name): Get variable from selected environment +// - roster.setVariable(name, value): Set/update variable +// - roster.setVariables({key: value}): Batch set +// - roster.project.name: Current project name +// - roster.project.environments: Array of environment names +// +// Modify the request before sending: +// request.headers['Authorization'] = 'Bearer ' + roster.getVariable('token'); +""" + preprocessing_buffer.set_text(placeholder_text) + preprocessing_scroll.set_child(self.preprocessing_sourceview) preprocessing_section.append(preprocessing_scroll) - info_label = Gtk.Label(label="Preprocessing will be available in a future release") - info_label.set_halign(Gtk.Align.START) - info_label.add_css_class("dim-label") - info_label.add_css_class("caption") - preprocessing_section.append(info_label) - paned.set_start_child(preprocessing_section) # Postprocessing Section (functional) @@ -570,6 +668,7 @@ class RequestTabWidget(Gtk.Box): scheme = scheme_manager.get_scheme('Adwaita-dark' if is_dark else 'Adwaita') if scheme: + self.preprocessing_sourceview.get_buffer().set_style_scheme(scheme) self.postprocessing_sourceview.get_buffer().set_style_scheme(scheme) def _on_script_changed(self, buffer): @@ -654,6 +753,9 @@ class RequestTabWidget(Gtk.Box): body_buffer.connect("changed", self._on_request_changed) # Setup script change tracking + preprocessing_buffer = self.preprocessing_sourceview.get_buffer() + preprocessing_buffer.connect("changed", self._on_request_changed) + postprocessing_buffer = self.postprocessing_sourceview.get_buffer() postprocessing_buffer.connect("changed", self._on_request_changed) @@ -924,6 +1026,92 @@ class RequestTabWidget(Gtk.Box): self.script_output_scroll.set_max_content_height(60) self.script_expander_label.set_text("Show full output") + def display_preprocessing_results(self, preprocessing_result): + """Display preprocessing execution results.""" + from .script_executor import ScriptResult + + if not isinstance(preprocessing_result, ScriptResult): + return + + # Show container + self.preprocessing_results_container.set_visible(True) + + # Set status icon + if preprocessing_result.success: + self.preprocessing_status_icon.set_from_icon_name("emblem-ok-symbolic") + output_text = preprocessing_result.output + else: + self.preprocessing_status_icon.set_from_icon_name("dialog-error-symbolic") + output_text = preprocessing_result.error if preprocessing_result.error else "Unknown error" + + # Append variable updates to output + if preprocessing_result.variable_updates: + if output_text: + output_text += "\n" + output_text += "\n--- Variables Set ---" + for var_name, var_value in preprocessing_result.variable_updates.items(): + # Truncate long values for display + display_value = var_value if len(var_value) <= 50 else var_value[:47] + "..." + output_text += f"\n>>> {var_name} = '{display_value}'" + + # Append warnings to output + if preprocessing_result.warnings: + if output_text: + output_text += "\n" + output_text += "\n--- Warnings ---" + for warning in preprocessing_result.warnings: + output_text += f"\n⚠ {warning}" + + # Add request modification summary if successful + if preprocessing_result.success and preprocessing_result.modified_request: + if output_text: + output_text += "\n" + output_text += "\n--- Request Modified ---" + output_text += "\n✓ Request was modified by preprocessing script" + + # Set output text + buffer = self.preprocessing_output_textview.get_buffer() + buffer.set_text(output_text) + + # Determine if expander is needed + num_lines = output_text.count('\n') + 1 + needs_expander = num_lines > 3 or len(output_text) > 150 + + if needs_expander: + self.preprocessing_expander.set_visible(True) + self.preprocessing_expander.set_cursor_from_name("pointer") + else: + self.preprocessing_expander.set_visible(False) + + # Reset expansion state + self.preprocessing_output_expanded = False + self.preprocessing_output_scroll.set_min_content_height(60) + self.preprocessing_output_scroll.set_max_content_height(60) + self.preprocessing_expander_label.set_text("Show full output") + + def _clear_preprocessing_results(self): + """Clear and hide preprocessing results panel.""" + self.preprocessing_results_container.set_visible(False) + buffer = self.preprocessing_output_textview.get_buffer() + buffer.set_text("") + self.preprocessing_expander.set_visible(False) + self.preprocessing_output_expanded = False + + def _on_preprocessing_expander_clicked(self, gesture, n_press, x, y): + """Toggle preprocessing output expansion.""" + self.preprocessing_output_expanded = not self.preprocessing_output_expanded + + if self.preprocessing_output_expanded: + # Expand: set max first to avoid assertion errors + self.preprocessing_output_scroll.set_max_content_height(300) + self.preprocessing_output_scroll.set_min_content_height(300) + self.preprocessing_expander_label.set_text("Collapse output") + else: + # Collapse: set min first to avoid assertion errors + self.preprocessing_output_scroll.set_min_content_height(60) + self.preprocessing_output_scroll.set_max_content_height(60) + self.preprocessing_expander_label.set_text("Show full output") + def _extract_content_type(self, headers_text): """Extract content-type from response headers.""" if not headers_text: diff --git a/src/script_executor.py b/src/script_executor.py index bd1ef35..36a1956 100644 --- a/src/script_executor.py +++ b/src/script_executor.py @@ -27,6 +27,7 @@ from dataclasses import dataclass, field if TYPE_CHECKING: from .project_manager import ProjectManager + from .models import HttpRequest @dataclass @@ -37,6 +38,7 @@ class ScriptResult: error: Optional[str] = None variable_updates: Dict[str, str] = field(default_factory=dict) # Variables set by script warnings: List[str] = field(default_factory=list) # Warnings (e.g., no env selected) + modified_request: Optional['HttpRequest'] = None # Modified request from preprocessing @dataclass @@ -157,6 +159,171 @@ print(JSON.stringify(roster.__variables)); error_msg = error.strip() if error else "Script execution failed" return ScriptResult(success=False, output="", error=error_msg) + @staticmethod + def execute_preprocessing_script( + script_code: str, + request, + context: Optional[ScriptContext] = None + ) -> ScriptResult: + """ + Execute preprocessing script with request object. + + Args: + script_code: JavaScript code to execute + request: HttpRequest object (will be modified) + context: Optional context for variable access and updates + + Returns: + ScriptResult with output, modified request, variable updates, or error + """ + if not script_code.strip(): + return ScriptResult(success=True, output="") + + # Import here to avoid circular import + from .models import HttpRequest + + # Create request object for JavaScript + request_obj = ScriptExecutor._create_request_object(request) + + # Get environment variables (read-only) + env_vars = ScriptExecutor._get_environment_variables(context) + + # Get project metadata + project_meta = ScriptExecutor._get_project_metadata(context) + + # Build complete script with context + script_with_context = f""" +const request = {json.dumps(request_obj)}; + +// Roster API for variable management +const roster = {{ + __variables: {{}}, // For setVariable + __envVariables: {json.dumps(env_vars)}, // Read-only current environment + + getVariable: function(name) {{ + return this.__envVariables[name] || null; + }}, + + setVariable: function(name, value) {{ + this.__variables[String(name)] = String(value); + }}, + + setVariables: function(obj) {{ + for (let key in obj) {{ + if (obj.hasOwnProperty(key)) {{ + this.__variables[String(key)] = String(obj[key]); + }} + }} + }}, + + project: {json.dumps(project_meta)} +}}; + +// Capture console.log output +let consoleOutput = []; +const console = {{ + log: function(...args) {{ + consoleOutput.push(args.map(arg => String(arg)).join(' ')); + }} +}}; + +// User script +try {{ +{script_code} +}} catch (error) {{ + consoleOutput.push('Error: ' + error.message); + throw error; +}} + +// Output results: modified request + separator + console logs + variables +print(JSON.stringify(request)); +print('___ROSTER_SEPARATOR___'); +print(consoleOutput.join('\\n')); +print('___ROSTER_VARIABLES___'); +print(JSON.stringify(roster.__variables)); +""" + + # Execute with gjs + output, error, returncode = ScriptExecutor._run_gjs_script( + script_with_context, + timeout=ScriptExecutor.TIMEOUT_SECONDS + ) + + # Parse output and extract results + console_output = "" + variable_updates = {} + warnings = [] + modified_request = None + + if returncode == 0: + # Split output into: request JSON + console logs + variables JSON + parts = output.split('___ROSTER_SEPARATOR___') + if len(parts) >= 2: + request_json_str = parts[0].strip() + rest = parts[1] + + # Parse modified request + try: + request_data = json.loads(request_json_str) + + # Validate required fields + if not request_data.get('url') or not request_data.get('url').strip(): + warnings.append("Modified request has empty URL") + return ScriptResult( + success=False, + output="", + error="Script produced invalid request: URL is empty", + warnings=warnings + ) + + # Create HttpRequest from modified data + modified_request = HttpRequest( + method=request_data.get('method', 'GET'), + url=request_data.get('url', ''), + headers=request_data.get('headers', {}), + body=request_data.get('body', ''), + syntax=request.syntax # Preserve syntax from original + ) + + except json.JSONDecodeError as e: + warnings.append(f"Failed to parse modified request: {str(e)}") + return ScriptResult( + success=False, + output="", + error="Script produced invalid request JSON", + warnings=warnings + ) + + # Parse console output and variables + var_parts = rest.split('___ROSTER_VARIABLES___') + console_output = var_parts[0].strip() + + if len(var_parts) > 1: + try: + variables_json = var_parts[1].strip() + raw_variables = json.loads(variables_json) + + # Validate variable names and add to updates + for name, value in raw_variables.items(): + if ScriptExecutor._is_valid_variable_name(name): + variable_updates[name] = value + else: + warnings.append(f"Invalid variable name '{name}' (must be alphanumeric + underscore)") + + except json.JSONDecodeError: + warnings.append("Failed to parse variable updates from script") + + return ScriptResult( + success=True, + output=console_output, + variable_updates=variable_updates, + warnings=warnings, + modified_request=modified_request + ) + else: + error_msg = error.strip() if error else "Script execution failed" + return ScriptResult(success=False, output="", error=error_msg) + @staticmethod def _create_response_object(response) -> dict: """Convert HttpResponse to JavaScript-compatible dict.""" @@ -177,6 +344,47 @@ print(JSON.stringify(roster.__variables)); 'responseTime': response.response_time_ms } + @staticmethod + def _create_request_object(request) -> dict: + """Convert HttpRequest to JavaScript-compatible dict.""" + return { + 'method': request.method, + 'url': request.url, + 'headers': dict(request.headers), # Convert to regular dict + 'body': request.body + } + + @staticmethod + def _get_environment_variables(context: Optional[ScriptContext]) -> dict: + """Get environment variables from context.""" + if not context or not context.project_id or not context.environment_id: + return {} + + # Load projects and find the environment + projects = context.project_manager.load_projects() + for project in projects: + if project.id == context.project_id: + for env in project.environments: + if env.id == context.environment_id: + return env.variables + return {} + + @staticmethod + def _get_project_metadata(context: Optional[ScriptContext]) -> dict: + """Get project metadata (name and environments).""" + if not context or not context.project_id: + return {'name': '', 'environments': []} + + # Load projects and find the project + projects = context.project_manager.load_projects() + for project in projects: + if project.id == context.project_id: + return { + 'name': project.name, + 'environments': [env.name for env in project.environments] + } + return {'name': '', 'environments': []} + @staticmethod def _is_valid_variable_name(name: str) -> bool: """ diff --git a/src/window.py b/src/window.py index 4409aed..4188ecd 100644 --- a/src/window.py +++ b/src/window.py @@ -434,19 +434,59 @@ class RosterWindow(Adw.ApplicationWindow): def _on_send_clicked(self, widget): """Handle Send button click from a tab widget.""" + # Clear previous preprocessing results + if hasattr(widget, '_clear_preprocessing_results'): + widget._clear_preprocessing_results() + # Validate URL request = widget.get_request() if not request.url.strip(): self._show_toast("Please enter a URL") return - # Apply variable substitution if environment is selected - substituted_request = request + # Execute preprocessing script (if exists) + scripts = widget.get_scripts() + modified_request = request + + if scripts and scripts.preprocessing.strip(): + from .script_executor import ScriptContext + + preprocessing_result = self._execute_preprocessing( + widget, scripts.preprocessing, request + ) + + # Display preprocessing results + if hasattr(widget, 'display_preprocessing_results'): + widget.display_preprocessing_results(preprocessing_result) + + # Handle errors - DON'T send request if preprocessing failed + if not preprocessing_result.success: + self._show_toast(f"Preprocessing error: {preprocessing_result.error}") + return # Early return, don't send request + + # Create context for variable updates + context = None + if widget.project_id and widget.selected_environment_id: + context = ScriptContext( + project_id=widget.project_id, + environment_id=widget.selected_environment_id, + project_manager=self.project_manager + ) + + # Process variable updates from preprocessing + self._process_variable_updates(preprocessing_result, widget, context) + + # Use modified request (if returned) + if preprocessing_result.modified_request: + modified_request = preprocessing_result.modified_request + + # Apply variable substitution to (possibly modified) request + substituted_request = modified_request if widget.selected_environment_id: env = widget.get_selected_environment() if env: from .variable_substitution import VariableSubstitution - substituted_request, undefined = VariableSubstitution.substitute_request(request, env) + substituted_request, undefined = VariableSubstitution.substitute_request(modified_request, env) # Log undefined variables for debugging if undefined: print(f"Warning: Undefined variables in request: {', '.join(undefined)}") @@ -596,6 +636,36 @@ class RosterWindow(Adw.ApplicationWindow): self._show_toast("Request loaded from history") + def _execute_preprocessing(self, widget, script_code, request): + """ + Execute preprocessing script and return result. + + Args: + widget: RequestTabWidget instance + script_code: JavaScript preprocessing script + request: HttpRequest to be preprocessed + + Returns: + ScriptResult with modified request and variable updates + """ + from .script_executor import ScriptExecutor, ScriptContext + + # Create context for variable access + context = None + if widget.project_id and widget.selected_environment_id: + context = ScriptContext( + project_id=widget.project_id, + environment_id=widget.selected_environment_id, + project_manager=self.project_manager + ) + + # Execute preprocessing + return ScriptExecutor.execute_preprocessing_script( + script_code, + request, + context + ) + def _process_variable_updates(self, script_result, widget, context): """ Process variable updates from postprocessing script.