webtap-tool 0.7.0__py3-none-any.whl → 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of webtap-tool might be problematic. Click here for more details.

@@ -0,0 +1,268 @@
1
+ """Generate types and schemas from HTTP response bodies using quicktype."""
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from webtap.app import app
9
+ from webtap.commands._builders import check_connection, success_response, error_response
10
+ from webtap.commands._code_generation import ensure_output_directory
11
+ from webtap.commands._tips import get_mcp_description
12
+
13
+
14
+ mcp_desc = get_mcp_description("quicktype")
15
+
16
+ # Header template for generated files
17
+ HEADER_TEMPLATE = """Generated by WebTap from event {event_id}
18
+ Source: {url}
19
+ Method: {method}
20
+ Generated: {timestamp}
21
+
22
+ Do not edit manually."""
23
+
24
+ # Comment syntax per language
25
+ COMMENT_STYLES = {
26
+ "TypeScript": "//",
27
+ "Python": "#",
28
+ "Go": "//",
29
+ "Rust": "//",
30
+ "Java": "//",
31
+ "Kotlin": "//",
32
+ "Swift": "//",
33
+ "C#": "//",
34
+ "C++": "//",
35
+ "Dart": "//",
36
+ "Ruby": "#",
37
+ "JSON Schema": None, # No comments in JSON
38
+ }
39
+
40
+
41
+ def _run_quicktype(
42
+ json_data: dict | list,
43
+ output: str,
44
+ type_name: str = None, # pyright: ignore[reportArgumentType]
45
+ just_types: bool = True,
46
+ prefer_types: bool = True,
47
+ options: dict = None, # pyright: ignore[reportArgumentType]
48
+ ) -> tuple[bool, str]:
49
+ """Run quicktype CLI with given parameters.
50
+
51
+ Args:
52
+ json_data: JSON data to convert
53
+ output: Output file path
54
+ type_name: Top-level type name
55
+ just_types: Generate only types, no serializers
56
+ prefer_types: Use 'type' instead of 'interface' for TypeScript
57
+ options: Additional quicktype flags
58
+
59
+ Returns:
60
+ Tuple of (success: bool, error_message: str)
61
+ """
62
+ # Check if quicktype is available
63
+ if not shutil.which("quicktype"):
64
+ return False, "quicktype CLI not found. Install with: npm install -g quicktype"
65
+
66
+ # Determine language from file extension
67
+ output_path = Path(output)
68
+ ext = output_path.suffix.lower()
69
+
70
+ # Build command
71
+ cmd = ["quicktype", "-o", str(output), "--src-lang", "json", "--top-level", type_name]
72
+
73
+ # Add opinionated defaults
74
+ if just_types:
75
+ cmd.append("--just-types")
76
+
77
+ # TypeScript-specific options
78
+ if ext in {".ts", ".tsx"} and prefer_types:
79
+ cmd.append("--prefer-types")
80
+
81
+ # Apply additional options
82
+ for key, val in (options or {}).items():
83
+ flag = f"--{key.replace('_', '-')}"
84
+ if val is True:
85
+ cmd.append(flag)
86
+ elif val is not False and val is not None:
87
+ cmd.extend([flag, str(val)])
88
+
89
+ # Run quicktype
90
+ try:
91
+ subprocess.run(
92
+ cmd,
93
+ input=json.dumps(json_data, indent=2),
94
+ capture_output=True,
95
+ text=True,
96
+ check=True,
97
+ timeout=30,
98
+ )
99
+ return True, ""
100
+ except subprocess.CalledProcessError as e:
101
+ return False, f"quicktype failed: {e.stderr.strip() if e.stderr else str(e)}"
102
+ except subprocess.TimeoutExpired:
103
+ return False, "quicktype timed out (>30s)"
104
+ except Exception as e:
105
+ return False, f"Unexpected error: {str(e)}"
106
+
107
+
108
+ def _insert_header(state, event: int, output_path: Path, language: str) -> None:
109
+ """Insert language-aware header comment into generated file.
110
+
111
+ Args:
112
+ state: WebTap state with CDP session
113
+ event: Event row ID for metadata extraction
114
+ output_path: Path to generated file
115
+ language: Target language (e.g., "TypeScript", "Python")
116
+ """
117
+ if not HEADER_TEMPLATE or language not in COMMENT_STYLES:
118
+ return
119
+
120
+ comment_prefix = COMMENT_STYLES[language]
121
+ if not comment_prefix: # Skip languages without comments (e.g., JSON)
122
+ return
123
+
124
+ try:
125
+ # Get event metadata
126
+ event_result = state.cdp.query("SELECT event FROM events WHERE rowid = ?", [event])
127
+ if not event_result:
128
+ return
129
+
130
+ event_data = json.loads(event_result[0][0])
131
+ params = event_data.get("params", {})
132
+
133
+ # Extract metadata from event
134
+ request_data = params.get("request", {})
135
+ response_data = params.get("response", {})
136
+
137
+ metadata = {
138
+ "event_id": event,
139
+ "url": request_data.get("url") or response_data.get("url", "N/A"),
140
+ "method": request_data.get("method", "N/A"),
141
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
142
+ }
143
+
144
+ # Format header text with metadata
145
+ header_text = HEADER_TEMPLATE.format(**metadata)
146
+
147
+ # Apply comment syntax to each line
148
+ header_lines = [
149
+ f"{comment_prefix} {line}" if line.strip() else comment_prefix for line in header_text.split("\n")
150
+ ]
151
+ header = "\n".join(header_lines)
152
+
153
+ # Prepend header to file
154
+ content = output_path.read_text()
155
+ output_path.write_text(header + "\n\n" + content)
156
+ except Exception:
157
+ # Silent failure - don't break generation if header fails
158
+ pass
159
+
160
+
161
+ @app.command(display="markdown", fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"})
162
+ def quicktype(
163
+ state,
164
+ event: int,
165
+ output: str,
166
+ type_name: str,
167
+ json_path: str = None, # pyright: ignore[reportArgumentType]
168
+ expr: str = None, # pyright: ignore[reportArgumentType]
169
+ just_types: bool = True,
170
+ prefer_types: bool = True,
171
+ options: dict = None, # pyright: ignore[reportArgumentType]
172
+ ) -> dict: # pyright: ignore[reportArgumentType]
173
+ """Generate types/schemas from request or response body using quicktype CLI.
174
+
175
+ Args:
176
+ event: Event row ID from network() or events()
177
+ output: Output file path (extension determines language: .ts, .py, .go, etc.)
178
+ type_name: Top-level type name (e.g., "User", "ApiResponse")
179
+ json_path: Optional JSON path to extract nested data (e.g., "data[0]")
180
+ expr: Optional Python expression to transform data (has 'body' and 'event' variables)
181
+ just_types: Generate only types, no serializers (default: True)
182
+ prefer_types: Use 'type' instead of 'interface' for TypeScript (default: True)
183
+ options: Additional quicktype flags as dict (e.g., {"readonly": True, "nice_property_names": True})
184
+
185
+ Examples:
186
+ quicktype(123, "types/User.ts", "User") # TypeScript interface
187
+ quicktype(123, "models/customer.py", "Customer") # Python dataclass
188
+ quicktype(123, "api.go", "ApiResponse") # Go struct
189
+ quicktype(123, "schema.json", "Schema") # JSON Schema
190
+ quicktype(123, "types.ts", "User", json_path="data[0]") # Extract nested
191
+ quicktype(172, "types/Form.ts", "Form", expr="dict(urllib.parse.parse_qsl(body))") # Parse form data
192
+ quicktype(123, "types.ts", "User", options={"readonly": True}) # Advanced options
193
+
194
+ Returns:
195
+ Success message with generation details
196
+ """
197
+ if error := check_connection(state):
198
+ return error
199
+
200
+ # Prepare data via service layer
201
+ result = state.service.body.prepare_for_generation(event, json_path, expr)
202
+ if result.get("error"):
203
+ return error_response(result["error"], suggestions=result.get("suggestions", []))
204
+
205
+ data = result["data"]
206
+
207
+ # Ensure output directory exists
208
+ output_path = ensure_output_directory(output)
209
+
210
+ # Run quicktype
211
+ success, error_msg = _run_quicktype(
212
+ json_data=data,
213
+ output=str(output_path),
214
+ type_name=type_name,
215
+ just_types=just_types,
216
+ prefer_types=prefer_types,
217
+ options=options,
218
+ )
219
+
220
+ if not success:
221
+ return error_response(
222
+ error_msg,
223
+ suggestions=[
224
+ "Check that quicktype is installed: npm install -g quicktype",
225
+ "Verify the JSON structure is valid",
226
+ "Try simplifying the data with json_path",
227
+ ],
228
+ )
229
+
230
+ # Detect language from extension
231
+ ext = output_path.suffix.lower()
232
+ lang_map = {
233
+ ".ts": "TypeScript",
234
+ ".tsx": "TypeScript",
235
+ ".py": "Python",
236
+ ".go": "Go",
237
+ ".rs": "Rust",
238
+ ".java": "Java",
239
+ ".kt": "Kotlin",
240
+ ".swift": "Swift",
241
+ ".cs": "C#",
242
+ ".cpp": "C++",
243
+ ".dart": "Dart",
244
+ ".rb": "Ruby",
245
+ ".json": "JSON Schema",
246
+ }
247
+ language = lang_map.get(ext, "Unknown")
248
+
249
+ # Add header comment with event metadata
250
+ _insert_header(state, event, output_path, language)
251
+
252
+ # Count lines in generated file
253
+ try:
254
+ file_content = output_path.read_text()
255
+ line_count = len(file_content.splitlines())
256
+ except Exception:
257
+ line_count = "unknown"
258
+
259
+ return success_response(
260
+ "Types generated successfully",
261
+ details={
262
+ "Output": str(output_path),
263
+ "Language": language,
264
+ "Type Name": type_name,
265
+ "Lines": line_count,
266
+ "Size": f"{output_path.stat().st_size} bytes",
267
+ },
268
+ )
@@ -1,10 +1,10 @@
1
1
  """Generate Pydantic models from HTTP response bodies."""
2
2
 
3
3
  import json
4
- from pathlib import Path
5
4
  from datamodel_code_generator import generate, InputFileType, DataModelType
6
5
  from webtap.app import app
7
6
  from webtap.commands._builders import check_connection, success_response, error_response
7
+ from webtap.commands._code_generation import ensure_output_directory
8
8
  from webtap.commands._tips import get_mcp_description
9
9
 
10
10
 
@@ -12,14 +12,20 @@ mcp_desc = get_mcp_description("to_model")
12
12
 
13
13
 
14
14
  @app.command(display="markdown", fastmcp={"type": "tool", "description": mcp_desc} if mcp_desc else {"type": "tool"})
15
- def to_model(state, response: int, output: str, model_name: str, json_path: str = None) -> dict: # pyright: ignore[reportArgumentType]
16
- """Generate Pydantic model from response body using datamodel-codegen.
15
+ def to_model(state, event: int, output: str, model_name: str, json_path: str = None, expr: str = None) -> dict: # pyright: ignore[reportArgumentType]
16
+ """Generate Pydantic model from request or response body using datamodel-codegen.
17
17
 
18
18
  Args:
19
- response: Response row ID from network() table
19
+ event: Event row ID from network() or events()
20
20
  output: Output file path for generated model (e.g., "models/customers/group.py")
21
21
  model_name: Class name for generated model (e.g., "CustomerGroup")
22
- json_path: Optional JSON path to extract nested data (e.g., "Data[0]")
22
+ json_path: Optional JSON path to extract nested data (e.g., "data[0]")
23
+ expr: Optional Python expression to transform data (has 'body' and 'event' variables)
24
+
25
+ Examples:
26
+ to_model(123, "models/user.py", "User", json_path="data[0]")
27
+ to_model(172, "models/form.py", "Form", expr="dict(urllib.parse.parse_qsl(body))")
28
+ to_model(123, "models/clean.py", "Clean", expr="{k: v for k, v in json.loads(body).items() if k != 'meta'}")
23
29
 
24
30
  Returns:
25
31
  Success message with generation details
@@ -27,73 +33,15 @@ def to_model(state, response: int, output: str, model_name: str, json_path: str
27
33
  if error := check_connection(state):
28
34
  return error
29
35
 
30
- # Get response body from service
31
- body_service = state.service.body
32
- result = body_service.get_response_body(response, use_cache=True)
33
-
34
- if "error" in result:
35
- return error_response(result["error"])
36
-
37
- body_content = result.get("body", "")
38
- is_base64 = result.get("base64Encoded", False)
39
-
40
- # Decode if needed
41
- if is_base64:
42
- decoded = body_service.decode_body(body_content, is_base64)
43
- if isinstance(decoded, bytes):
44
- return error_response(
45
- "Response body is binary",
46
- suggestions=["Only JSON responses can be converted to models", "Try a different response"],
47
- )
48
- body_content = decoded
36
+ # Prepare data via service layer
37
+ result = state.service.body.prepare_for_generation(event, json_path, expr)
38
+ if result.get("error"):
39
+ return error_response(result["error"], suggestions=result.get("suggestions", []))
49
40
 
50
- # Parse JSON
51
- try:
52
- data = json.loads(body_content)
53
- except json.JSONDecodeError as e:
54
- return error_response(
55
- f"Invalid JSON: {e}",
56
- suggestions=[
57
- "Response must be valid JSON",
58
- "Check the response with body() first",
59
- "Try a different response",
60
- ],
61
- )
62
-
63
- # Extract JSON path if specified
64
- if json_path:
65
- try:
66
- # Support simple bracket notation like "Data[0]"
67
- parts = json_path.replace("[", ".").replace("]", "").split(".")
68
- for part in parts:
69
- if part:
70
- if part.isdigit():
71
- data = data[int(part)]
72
- else:
73
- data = data[part]
74
- except (KeyError, IndexError, TypeError) as e:
75
- return error_response(
76
- f"JSON path extraction failed: {e}",
77
- suggestions=[
78
- f"Path '{json_path}' not found in response",
79
- "Check the response structure with body()",
80
- 'Try a simpler path like "Data" or "Data[0]"',
81
- ],
82
- )
83
-
84
- # Ensure data is dict or list for model generation
85
- if not isinstance(data, (dict, list)):
86
- return error_response(
87
- f"Extracted data is {type(data).__name__}, not dict or list",
88
- suggestions=[
89
- "Model generation requires dict or list structure",
90
- "Adjust json_path to extract a complex object",
91
- ],
92
- )
41
+ data = result["data"]
93
42
 
94
- # Create output directory if needed
95
- output_path = Path(output).expanduser().resolve()
96
- output_path.parent.mkdir(parents=True, exist_ok=True)
43
+ # Ensure output directory exists
44
+ output_path = ensure_output_directory(output)
97
45
 
98
46
  # Generate model using datamodel-codegen Python API
99
47
  try:
@@ -103,10 +51,10 @@ def to_model(state, response: int, output: str, model_name: str, json_path: str
103
51
  input_filename="response.json",
104
52
  output=output_path,
105
53
  output_model_type=DataModelType.PydanticV2BaseModel,
106
- class_name=model_name, # Set generated class name
107
- snake_case_field=True, # Convert to snake_case
108
- use_standard_collections=True, # Use list instead of List
109
- use_union_operator=True, # Use | instead of Union
54
+ class_name=model_name,
55
+ snake_case_field=True,
56
+ use_standard_collections=True,
57
+ use_union_operator=True,
110
58
  )
111
59
  except Exception as e:
112
60
  return error_response(
@@ -121,7 +69,7 @@ def to_model(state, response: int, output: str, model_name: str, json_path: str
121
69
  # Count fields in generated model
122
70
  try:
123
71
  model_content = output_path.read_text()
124
- field_count = model_content.count(": ") # Count field definitions
72
+ field_count = model_content.count(": ")
125
73
  except Exception:
126
74
  field_count = "unknown"
127
75
 
webtap/services/body.py CHANGED
@@ -19,17 +19,25 @@ class BodyService:
19
19
  self.cdp: CDPSession | None = None
20
20
  self._body_cache: dict[str, dict] = {}
21
21
 
22
- def get_response_body(self, rowid: int, use_cache: bool = True) -> dict:
23
- """Fetch response body for a response.
22
+ def get_body(self, rowid: int, use_cache: bool = True) -> dict:
23
+ """Fetch request or response body for an event.
24
+
25
+ Automatically detects event type and fetches appropriate body:
26
+ - Network.requestWillBeSent: request body (POST data)
27
+ - Network.responseReceived: response body
28
+ - Fetch.requestPaused: request or response body based on stage
24
29
 
25
30
  Args:
26
- rowid: Row ID from events table (Network or Fetch response)
31
+ rowid: Row ID from events table
27
32
  use_cache: Whether to use cached body if available
33
+
34
+ Returns:
35
+ Dict with 'body' (str), 'base64Encoded' (bool), and 'event' (dict), or 'error' (str)
28
36
  """
29
37
  if not self.cdp:
30
38
  return {"error": "No CDP session"}
31
39
 
32
- # Get event from DB to extract requestId
40
+ # Get event from DB to extract requestId and method
33
41
  result = self.cdp.query("SELECT event FROM events WHERE rowid = ?", [rowid])
34
42
 
35
43
  if not result:
@@ -42,45 +50,89 @@ class BodyService:
42
50
 
43
51
  method = event_data.get("method", "")
44
52
  params = event_data.get("params", {})
53
+ request_id = params.get("requestId")
54
+
55
+ if not request_id:
56
+ return {"error": "No requestId in event"}
57
+
58
+ # Check cache first (cache includes event_data)
59
+ cache_key = f"{request_id}:{method}"
60
+ if use_cache and cache_key in self._body_cache:
61
+ logger.debug(f"Using cached body for {cache_key}")
62
+ return self._body_cache[cache_key]
63
+
64
+ # Handle request body (POST data)
65
+ if method == "Network.requestWillBeSent":
66
+ request = params.get("request", {})
67
+
68
+ # Check inline postData first (may be present for small bodies)
69
+ if request.get("postData"):
70
+ logger.debug(f"Using inline postData for {request_id}")
71
+ body_data = {"body": request["postData"], "base64Encoded": False, "event": event_data}
72
+ if use_cache:
73
+ self._body_cache[cache_key] = body_data
74
+ return body_data
75
+
76
+ # Check if request has POST data
77
+ if not request.get("hasPostData"):
78
+ return {"error": "No POST data in this request (GET or no body)"}
79
+
80
+ # Try to fetch POST data via CDP
81
+ try:
82
+ logger.debug(f"Fetching POST data for {request_id} using Network.getRequestPostData")
83
+ result = self.cdp.execute("Network.getRequestPostData", {"requestId": request_id})
84
+ body_data = {"body": result.get("postData", ""), "base64Encoded": False, "event": event_data}
85
+
86
+ if use_cache:
87
+ self._body_cache[cache_key] = body_data
88
+ logger.debug(f"Cached POST data for {request_id}")
89
+
90
+ return body_data
45
91
 
46
- # Handle both Fetch and Network events
47
- if method == "Fetch.requestPaused":
92
+ except Exception as e:
93
+ error_msg = str(e)
94
+ logger.error(f"Failed to fetch POST data for {request_id}: {error_msg}")
95
+ # Provide helpful error message
96
+ if "No resource with given identifier found" in error_msg:
97
+ return {"error": "POST data not available (possibly too large or not captured by CDP)"}
98
+ return {"error": f"Failed to fetch POST data: {error_msg}"}
99
+
100
+ # Handle response body
101
+ elif method == "Fetch.requestPaused":
48
102
  # Fetch interception - verify it's response stage
49
103
  if "responseStatusCode" not in params:
50
104
  return {"error": "Not a response stage event (no responseStatusCode)"}
51
- request_id = params.get("requestId")
52
105
  domain = "Fetch"
106
+
53
107
  elif method == "Network.responseReceived":
54
108
  # Regular network response
55
- request_id = params.get("requestId")
56
109
  domain = "Network"
57
- else:
58
- return {"error": f"Not a response event (method: {method})"}
59
110
 
60
- if not request_id:
61
- return {"error": "No requestId in event"}
62
-
63
- # Check cache
64
- if use_cache and request_id in self._body_cache:
65
- logger.debug(f"Using cached body for {request_id}")
66
- return self._body_cache[request_id]
111
+ else:
112
+ # Unknown event type - return empty body but include event for expr access
113
+ logger.debug(f"Unknown event type {method} - returning empty body with event data")
114
+ return {"body": "", "base64Encoded": False, "event": event_data}
67
115
 
116
+ # Fetch response body from CDP
68
117
  try:
69
- # Fetch body from CDP using appropriate domain
70
- logger.debug(f"Fetching body for {request_id} using {domain}.getResponseBody")
118
+ logger.debug(f"Fetching response body for {request_id} using {domain}.getResponseBody")
71
119
  result = self.cdp.execute(f"{domain}.getResponseBody", {"requestId": request_id})
72
120
 
73
- body_data = {"body": result.get("body", ""), "base64Encoded": result.get("base64Encoded", False)}
121
+ body_data = {
122
+ "body": result.get("body", ""),
123
+ "base64Encoded": result.get("base64Encoded", False),
124
+ "event": event_data,
125
+ }
74
126
 
75
- # Cache it for this request
127
+ # Cache it
76
128
  if use_cache:
77
- self._body_cache[request_id] = body_data
78
- logger.debug(f"Cached body for {request_id}")
129
+ self._body_cache[cache_key] = body_data
130
+ logger.debug(f"Cached response body for {request_id}")
79
131
 
80
132
  return body_data
81
133
 
82
134
  except Exception as e:
83
- logger.error(f"Failed to fetch body for {request_id}: {e}")
135
+ logger.error(f"Failed to fetch response body for {request_id}: {e}")
84
136
  return {"error": str(e)}
85
137
 
86
138
  def clear_cache(self):
@@ -111,3 +163,136 @@ class BodyService:
111
163
  except Exception as e:
112
164
  logger.error(f"Failed to decode base64 body: {e}")
113
165
  return body_content # Return original if decode fails
166
+
167
+ def prepare_for_generation(
168
+ self,
169
+ event: int,
170
+ json_path: str = None, # pyright: ignore[reportArgumentType]
171
+ expr: str = None, # pyright: ignore[reportArgumentType]
172
+ ) -> dict:
173
+ """Prepare HTTP body for code generation.
174
+
175
+ Orchestrates the complete pipeline:
176
+ 1. Fetch body + event from CDP
177
+ 2. Decode base64 if needed
178
+ 3. Transform via expression OR validate and parse JSON
179
+ 4. Extract nested data via json_path
180
+ 5. Validate data structure (dict/list)
181
+
182
+ Args:
183
+ event: Event row ID from network() or events()
184
+ json_path: Optional JSON path for nested extraction (e.g., "data[0]")
185
+ expr: Optional Python expression with 'body' and 'event' variables
186
+
187
+ Returns:
188
+ Dict with 'data' (dict|list) on success, or 'error' (str) on failure.
189
+ May include 'suggestions' (list[str]) for error guidance.
190
+
191
+ Examples:
192
+ result = body_service.prepare_for_generation(123, json_path="data[0]")
193
+ if result.get("error"):
194
+ return error_response(result["error"], suggestions=result.get("suggestions"))
195
+ data = result["data"]
196
+ """
197
+ # 1. Fetch body + event from CDP
198
+ result = self.get_body(event, use_cache=True)
199
+ if "error" in result:
200
+ return {"error": result["error"], "suggestions": [], "data": None}
201
+
202
+ body_content = result["body"]
203
+ is_base64 = result["base64Encoded"]
204
+ event_data = result["event"]
205
+
206
+ # 2. Decode if base64
207
+ if is_base64:
208
+ decoded = self.decode_body(body_content, is_base64)
209
+ if isinstance(decoded, bytes):
210
+ return {
211
+ "error": "Body is binary content",
212
+ "suggestions": [
213
+ "Only text/JSON can be converted to code",
214
+ "Try a different event with text content",
215
+ ],
216
+ "data": None,
217
+ }
218
+ body_content = decoded
219
+
220
+ # 3. Transform via expression OR validate and parse JSON
221
+ if expr:
222
+ # Use expression evaluation from _utils
223
+ from webtap.commands._utils import evaluate_expression
224
+
225
+ try:
226
+ namespace = {"body": body_content, "event": event_data}
227
+ data, _ = evaluate_expression(expr, namespace)
228
+ except Exception as e:
229
+ return {
230
+ "error": f"Expression evaluation failed: {e}",
231
+ "suggestions": [
232
+ "Check your expression syntax",
233
+ "Variables available: 'body' (str), 'event' (dict)",
234
+ "Example: dict(urllib.parse.parse_qsl(body))",
235
+ "Example: json.loads(body)['data'][0]",
236
+ ],
237
+ "data": None,
238
+ }
239
+ else:
240
+ # Validate body is not empty before parsing
241
+ if not body_content.strip():
242
+ return {
243
+ "error": "Body is empty",
244
+ "suggestions": [
245
+ "Use expr to extract data from event for non-HTTP events",
246
+ "Example: expr=\"json.loads(event['params']['response']['payloadData'])\"",
247
+ "Check the event structure with inspect() first",
248
+ ],
249
+ "data": None,
250
+ }
251
+
252
+ # Parse as JSON
253
+ from webtap.commands._code_generation import parse_json
254
+
255
+ data, error = parse_json(body_content)
256
+ if error:
257
+ return {
258
+ "error": error,
259
+ "suggestions": [
260
+ "Body must be valid JSON or use expr to transform it",
261
+ 'For form data: expr="dict(urllib.parse.parse_qsl(body))"',
262
+ "Check the body with body() command first",
263
+ ],
264
+ "data": None,
265
+ }
266
+
267
+ # 4. Extract nested path if specified
268
+ if json_path:
269
+ from webtap.commands._code_generation import extract_json_path
270
+
271
+ data, error = extract_json_path(data, json_path)
272
+ if error:
273
+ return {
274
+ "error": error,
275
+ "suggestions": [
276
+ f"Path '{json_path}' not found in body",
277
+ "Check the body structure with body() command",
278
+ 'Try a simpler path like "data" or "data[0]"',
279
+ ],
280
+ "data": None,
281
+ }
282
+
283
+ # 5. Validate structure
284
+ from webtap.commands._code_generation import validate_generation_data
285
+
286
+ is_valid, error = validate_generation_data(data)
287
+ if not is_valid:
288
+ return {
289
+ "error": error,
290
+ "suggestions": [
291
+ "Code generation requires dict or list structure",
292
+ "Adjust json_path to extract a complex object",
293
+ "Or use expr to transform data into dict/list",
294
+ ],
295
+ "data": None,
296
+ }
297
+
298
+ return {"data": data, "error": None, "suggestions": []}