Implement preprocessing scripts with request modification and variable access
This commit is contained in:
parent
c0878271d6
commit
68004c5bf4
160
README.md
160
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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
"""
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user