Implement preprocessing scripts with request modification and variable access

This commit is contained in:
Pavel Baksy 2026-01-02 01:04:24 +01:00
parent 37f1fd9be1
commit 1384e2c4fd
4 changed files with 641 additions and 15 deletions

160
README.md
View File

@ -8,6 +8,8 @@ A modern HTTP client for GNOME, built with GTK 4 and libadwaita.
- Configure custom headers and request bodies - Configure custom headers and request bodies
- View response headers and bodies - View response headers and bodies
- Track request history with persistence - Track request history with persistence
- **JavaScript preprocessing and postprocessing scripts**
- **Project environments with variables**
- Beautiful GNOME-native UI - Beautiful GNOME-native UI
## Dependencies ## Dependencies
@ -16,6 +18,7 @@ A modern HTTP client for GNOME, built with GTK 4 and libadwaita.
- libadwaita 1 - libadwaita 1
- Python 3 - Python 3
- libsoup3 (provided by GNOME Platform) - libsoup3 (provided by GNOME Platform)
- gjs (GNOME JavaScript) - for script execution
## Building ## Building
@ -30,3 +33,160 @@ sudo meson install -C builddir
Roster uses libsoup3 (from GNOME Platform) for making HTTP requests - no external dependencies required. 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. 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.

View File

@ -208,6 +208,91 @@ class RequestTabWidget(Gtk.Box):
body_scroll.set_child(self.response_body_sourceview) body_scroll.set_child(self.response_body_sourceview)
self.response_stack.add_titled(body_scroll, "body", "Body") 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) # Script Results Panel (collapsible)
self.script_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) self.script_results_container = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
self.script_results_container.set_visible(False) # Initially hidden self.script_results_container.set_visible(False) # Initially hidden
@ -425,27 +510,26 @@ class RequestTabWidget(Gtk.Box):
paned.set_position(300) paned.set_position(300)
paned.set_vexpand(True) paned.set_vexpand(True)
# Preprocessing Section (disabled for now) # Preprocessing Section
preprocessing_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) preprocessing_section = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
preprocessing_section.set_margin_start(12) preprocessing_section.set_margin_start(12)
preprocessing_section.set_margin_end(12) preprocessing_section.set_margin_end(12)
preprocessing_section.set_margin_top(12) preprocessing_section.set_margin_top(12)
preprocessing_section.set_margin_bottom(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.set_halign(Gtk.Align.START)
preprocessing_label.add_css_class("heading") preprocessing_label.add_css_class("heading")
preprocessing_label.add_css_class("dim-label")
preprocessing_section.append(preprocessing_label) preprocessing_section.append(preprocessing_label)
preprocessing_scroll = Gtk.ScrolledWindow() preprocessing_scroll = Gtk.ScrolledWindow()
preprocessing_scroll.set_vexpand(True) preprocessing_scroll.set_vexpand(True)
self.preprocessing_sourceview = GtkSource.View() self.preprocessing_sourceview = GtkSource.View()
self.preprocessing_sourceview.set_editable(False) self.preprocessing_sourceview.set_editable(True)
self.preprocessing_sourceview.set_sensitive(False) self.preprocessing_sourceview.set_sensitive(True)
self.preprocessing_sourceview.set_show_line_numbers(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_left_margin(12)
self.preprocessing_sourceview.set_right_margin(12) self.preprocessing_sourceview.set_right_margin(12)
self.preprocessing_sourceview.set_top_margin(12) self.preprocessing_sourceview.set_top_margin(12)
@ -471,15 +555,29 @@ class RequestTabWidget(Gtk.Box):
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION 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_scroll.set_child(self.preprocessing_sourceview)
preprocessing_section.append(preprocessing_scroll) 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) paned.set_start_child(preprocessing_section)
# Postprocessing Section (functional) # Postprocessing Section (functional)
@ -570,6 +668,7 @@ class RequestTabWidget(Gtk.Box):
scheme = scheme_manager.get_scheme('Adwaita-dark' if is_dark else 'Adwaita') scheme = scheme_manager.get_scheme('Adwaita-dark' if is_dark else 'Adwaita')
if scheme: if scheme:
self.preprocessing_sourceview.get_buffer().set_style_scheme(scheme)
self.postprocessing_sourceview.get_buffer().set_style_scheme(scheme) self.postprocessing_sourceview.get_buffer().set_style_scheme(scheme)
def _on_script_changed(self, buffer): def _on_script_changed(self, buffer):
@ -654,6 +753,9 @@ class RequestTabWidget(Gtk.Box):
body_buffer.connect("changed", self._on_request_changed) body_buffer.connect("changed", self._on_request_changed)
# Setup script change tracking # 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 = self.postprocessing_sourceview.get_buffer()
postprocessing_buffer.connect("changed", self._on_request_changed) 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_output_scroll.set_max_content_height(60)
self.script_expander_label.set_text("Show full output") 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): def _extract_content_type(self, headers_text):
"""Extract content-type from response headers.""" """Extract content-type from response headers."""
if not headers_text: if not headers_text:

View File

@ -27,6 +27,7 @@ from dataclasses import dataclass, field
if TYPE_CHECKING: if TYPE_CHECKING:
from .project_manager import ProjectManager from .project_manager import ProjectManager
from .models import HttpRequest
@dataclass @dataclass
@ -37,6 +38,7 @@ class ScriptResult:
error: Optional[str] = None error: Optional[str] = None
variable_updates: Dict[str, str] = field(default_factory=dict) # Variables set by script 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) warnings: List[str] = field(default_factory=list) # Warnings (e.g., no env selected)
modified_request: Optional['HttpRequest'] = None # Modified request from preprocessing
@dataclass @dataclass
@ -157,6 +159,171 @@ print(JSON.stringify(roster.__variables));
error_msg = error.strip() if error else "Script execution failed" error_msg = error.strip() if error else "Script execution failed"
return ScriptResult(success=False, output="", error=error_msg) 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 @staticmethod
def _create_response_object(response) -> dict: def _create_response_object(response) -> dict:
"""Convert HttpResponse to JavaScript-compatible dict.""" """Convert HttpResponse to JavaScript-compatible dict."""
@ -177,6 +344,47 @@ print(JSON.stringify(roster.__variables));
'responseTime': response.response_time_ms '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 @staticmethod
def _is_valid_variable_name(name: str) -> bool: def _is_valid_variable_name(name: str) -> bool:
""" """

View File

@ -434,19 +434,59 @@ class RosterWindow(Adw.ApplicationWindow):
def _on_send_clicked(self, widget): def _on_send_clicked(self, widget):
"""Handle Send button click from a tab 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 # Validate URL
request = widget.get_request() request = widget.get_request()
if not request.url.strip(): if not request.url.strip():
self._show_toast("Please enter a URL") self._show_toast("Please enter a URL")
return return
# Apply variable substitution if environment is selected # Execute preprocessing script (if exists)
substituted_request = request 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: if widget.selected_environment_id:
env = widget.get_selected_environment() env = widget.get_selected_environment()
if env: if env:
from .variable_substitution import VariableSubstitution 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 # Log undefined variables for debugging
if undefined: if undefined:
print(f"Warning: Undefined variables in request: {', '.join(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") 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): def _process_variable_updates(self, script_result, widget, context):
""" """
Process variable updates from postprocessing script. Process variable updates from postprocessing script.