@hustle-together/api-dev-tools 3.6.5 → 3.10.0
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.
- package/README.md +5599 -258
- package/bin/cli.js +395 -20
- package/commands/README.md +459 -71
- package/commands/hustle-api-continue.md +158 -0
- package/commands/{api-create.md → hustle-api-create.md} +35 -15
- package/commands/{api-env.md → hustle-api-env.md} +4 -4
- package/commands/{api-interview.md → hustle-api-interview.md} +1 -1
- package/commands/{api-research.md → hustle-api-research.md} +3 -3
- package/commands/hustle-api-sessions.md +149 -0
- package/commands/{api-status.md → hustle-api-status.md} +16 -16
- package/commands/{api-verify.md → hustle-api-verify.md} +2 -2
- package/commands/hustle-combine.md +763 -0
- package/commands/hustle-ui-create-page.md +933 -0
- package/commands/hustle-ui-create.md +825 -0
- package/hooks/api-workflow-check.py +545 -21
- package/hooks/cache-research.py +337 -0
- package/hooks/check-api-routes.py +168 -0
- package/hooks/check-playwright-setup.py +103 -0
- package/hooks/check-storybook-setup.py +81 -0
- package/hooks/detect-interruption.py +165 -0
- package/hooks/enforce-a11y-audit.py +202 -0
- package/hooks/enforce-brand-guide.py +241 -0
- package/hooks/enforce-documentation.py +60 -8
- package/hooks/enforce-freshness.py +184 -0
- package/hooks/enforce-page-components.py +186 -0
- package/hooks/enforce-page-data-schema.py +155 -0
- package/hooks/enforce-questions-sourced.py +146 -0
- package/hooks/enforce-schema-from-interview.py +248 -0
- package/hooks/enforce-ui-disambiguation.py +108 -0
- package/hooks/enforce-ui-interview.py +130 -0
- package/hooks/generate-manifest-entry.py +1161 -0
- package/hooks/session-logger.py +297 -0
- package/hooks/session-startup.py +160 -15
- package/hooks/track-scope-coverage.py +220 -0
- package/hooks/track-tool-use.py +81 -1
- package/hooks/update-api-showcase.py +149 -0
- package/hooks/update-registry.py +352 -0
- package/hooks/update-ui-showcase.py +212 -0
- package/package.json +8 -3
- package/templates/BRAND_GUIDE.md +299 -0
- package/templates/CLAUDE-SECTION.md +56 -24
- package/templates/SPEC.json +640 -0
- package/templates/api-dev-state.json +217 -161
- package/templates/api-showcase/_components/APICard.tsx +153 -0
- package/templates/api-showcase/_components/APIModal.tsx +375 -0
- package/templates/api-showcase/_components/APIShowcase.tsx +231 -0
- package/templates/api-showcase/_components/APITester.tsx +522 -0
- package/templates/api-showcase/page.tsx +41 -0
- package/templates/component/Component.stories.tsx +172 -0
- package/templates/component/Component.test.tsx +237 -0
- package/templates/component/Component.tsx +86 -0
- package/templates/component/Component.types.ts +55 -0
- package/templates/component/index.ts +15 -0
- package/templates/dev-tools/_components/DevToolsLanding.tsx +320 -0
- package/templates/dev-tools/page.tsx +10 -0
- package/templates/page/page.e2e.test.ts +218 -0
- package/templates/page/page.tsx +42 -0
- package/templates/performance-budgets.json +58 -0
- package/templates/registry.json +13 -0
- package/templates/settings.json +90 -0
- package/templates/shared/HeroHeader.tsx +261 -0
- package/templates/shared/index.ts +1 -0
- package/templates/ui-showcase/_components/PreviewCard.tsx +315 -0
- package/templates/ui-showcase/_components/PreviewModal.tsx +676 -0
- package/templates/ui-showcase/_components/UIShowcase.tsx +262 -0
- package/templates/ui-showcase/page.tsx +26 -0
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +0 -959
- package/demo/hustle-together/blog/interview-driven-api-development.html +0 -1146
- package/demo/hustle-together/blog/tdd-for-ai.html +0 -982
- package/demo/hustle-together/index.html +0 -1312
- package/demo/workflow-demo-v3.5-backup.html +0 -5008
- package/demo/workflow-demo.html +0 -6202
|
@@ -0,0 +1,1161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hook: PostToolUse for Write (triggered by cache-research.py or manually)
|
|
4
|
+
Purpose: Generate api-tests-manifest.json entry from Zod schema
|
|
5
|
+
|
|
6
|
+
This hook reads the created Zod schema file and generates a comprehensive
|
|
7
|
+
manifest entry that gets appended to api-tests-manifest.json.
|
|
8
|
+
|
|
9
|
+
Added in v3.6.7 for complete API documentation automation.
|
|
10
|
+
|
|
11
|
+
What it generates:
|
|
12
|
+
- Full requestSchema from Zod
|
|
13
|
+
- All parameters with types, required, descriptions
|
|
14
|
+
- Example curl commands for every parameter combination
|
|
15
|
+
- Test cases from the test file
|
|
16
|
+
- Response schema
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
- {"continue": true} - Always continues
|
|
20
|
+
"""
|
|
21
|
+
import json
|
|
22
|
+
import sys
|
|
23
|
+
import re
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
STATE_FILE = Path(__file__).parent.parent / "api-dev-state.json"
|
|
28
|
+
# Default manifest location - can be overridden
|
|
29
|
+
DEFAULT_MANIFEST = Path.cwd() / "src" / "app" / "api-test" / "api-tests-manifest.json"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_active_endpoint(state):
|
|
33
|
+
"""Get active endpoint - supports both old and new state formats."""
|
|
34
|
+
if "endpoints" in state and "active_endpoint" in state:
|
|
35
|
+
active = state.get("active_endpoint")
|
|
36
|
+
if active and active in state["endpoints"]:
|
|
37
|
+
return active, state["endpoints"][active]
|
|
38
|
+
return None, None
|
|
39
|
+
|
|
40
|
+
endpoint = state.get("endpoint")
|
|
41
|
+
if endpoint:
|
|
42
|
+
return endpoint, state
|
|
43
|
+
|
|
44
|
+
return None, None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_zod_schema(schema_content: str) -> dict:
|
|
48
|
+
"""Parse Zod schema content and extract all field information."""
|
|
49
|
+
properties = {}
|
|
50
|
+
required = []
|
|
51
|
+
|
|
52
|
+
# Match Zod object definitions
|
|
53
|
+
# Pattern: fieldName: z.type(...).chain()
|
|
54
|
+
field_pattern = r'(\w+):\s*z\.(\w+)\(([^)]*)\)([^\n,}]*)'
|
|
55
|
+
|
|
56
|
+
for match in re.finditer(field_pattern, schema_content):
|
|
57
|
+
field_name = match.group(1)
|
|
58
|
+
zod_type = match.group(2)
|
|
59
|
+
type_args = match.group(3)
|
|
60
|
+
chain = match.group(4) or ""
|
|
61
|
+
|
|
62
|
+
# Skip if it's not a real field
|
|
63
|
+
if field_name in {'z', 'const', 'export', 'type', 'schema'}:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
prop = {}
|
|
67
|
+
|
|
68
|
+
# Map Zod types to JSON Schema types
|
|
69
|
+
type_map = {
|
|
70
|
+
"string": "string",
|
|
71
|
+
"number": "number",
|
|
72
|
+
"boolean": "boolean",
|
|
73
|
+
"array": "array",
|
|
74
|
+
"object": "object",
|
|
75
|
+
"date": "string",
|
|
76
|
+
"bigint": "integer",
|
|
77
|
+
"any": "any",
|
|
78
|
+
"unknown": "any",
|
|
79
|
+
"null": "null",
|
|
80
|
+
"undefined": "null",
|
|
81
|
+
"void": "null",
|
|
82
|
+
"never": "null",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Handle enum
|
|
86
|
+
if zod_type == "enum":
|
|
87
|
+
prop["type"] = "string"
|
|
88
|
+
# Extract enum values: z.enum(["a", "b", "c"])
|
|
89
|
+
enum_match = re.search(r'\[([^\]]+)\]', type_args)
|
|
90
|
+
if enum_match:
|
|
91
|
+
enum_str = enum_match.group(1)
|
|
92
|
+
enum_values = re.findall(r'["\']([^"\']+)["\']', enum_str)
|
|
93
|
+
if enum_values:
|
|
94
|
+
prop["enum"] = enum_values
|
|
95
|
+
elif zod_type == "literal":
|
|
96
|
+
prop["type"] = "string"
|
|
97
|
+
literal_match = re.search(r'["\']([^"\']+)["\']', type_args)
|
|
98
|
+
if literal_match:
|
|
99
|
+
prop["const"] = literal_match.group(1)
|
|
100
|
+
elif zod_type == "union":
|
|
101
|
+
prop["type"] = "string" # Simplified
|
|
102
|
+
elif zod_type == "array":
|
|
103
|
+
prop["type"] = "array"
|
|
104
|
+
prop["items"] = {"type": "any"}
|
|
105
|
+
else:
|
|
106
|
+
prop["type"] = type_map.get(zod_type, "string")
|
|
107
|
+
|
|
108
|
+
# Check for optional
|
|
109
|
+
is_optional = ".optional()" in chain or ".nullish()" in chain
|
|
110
|
+
|
|
111
|
+
# Extract description
|
|
112
|
+
desc_match = re.search(r'\.describe\(["\']([^"\']+)["\']', chain)
|
|
113
|
+
if desc_match:
|
|
114
|
+
prop["description"] = desc_match.group(1)
|
|
115
|
+
|
|
116
|
+
# Extract default
|
|
117
|
+
default_match = re.search(r'\.default\(([^)]+)\)', chain)
|
|
118
|
+
if default_match:
|
|
119
|
+
default_val = default_match.group(1).strip()
|
|
120
|
+
# Try to parse the default value
|
|
121
|
+
if default_val.startswith('"') or default_val.startswith("'"):
|
|
122
|
+
prop["default"] = default_val.strip('"\'')
|
|
123
|
+
elif default_val == "true":
|
|
124
|
+
prop["default"] = True
|
|
125
|
+
elif default_val == "false":
|
|
126
|
+
prop["default"] = False
|
|
127
|
+
elif default_val.isdigit():
|
|
128
|
+
prop["default"] = int(default_val)
|
|
129
|
+
else:
|
|
130
|
+
try:
|
|
131
|
+
prop["default"] = float(default_val)
|
|
132
|
+
except ValueError:
|
|
133
|
+
prop["default"] = default_val
|
|
134
|
+
|
|
135
|
+
# Extract min/max for numbers
|
|
136
|
+
min_match = re.search(r'\.min\((\d+)\)', chain)
|
|
137
|
+
if min_match:
|
|
138
|
+
prop["minimum"] = int(min_match.group(1))
|
|
139
|
+
|
|
140
|
+
max_match = re.search(r'\.max\((\d+)\)', chain)
|
|
141
|
+
if max_match:
|
|
142
|
+
prop["maximum"] = int(max_match.group(1))
|
|
143
|
+
|
|
144
|
+
# Extract minLength/maxLength for strings
|
|
145
|
+
min_len_match = re.search(r'\.min\((\d+)\)', chain)
|
|
146
|
+
if min_len_match and prop.get("type") == "string":
|
|
147
|
+
prop["minLength"] = int(min_len_match.group(1))
|
|
148
|
+
|
|
149
|
+
max_len_match = re.search(r'\.max\((\d+)\)', chain)
|
|
150
|
+
if max_len_match and prop.get("type") == "string":
|
|
151
|
+
prop["maxLength"] = int(max_len_match.group(1))
|
|
152
|
+
|
|
153
|
+
properties[field_name] = prop
|
|
154
|
+
|
|
155
|
+
if not is_optional:
|
|
156
|
+
required.append(field_name)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
"type": "object",
|
|
160
|
+
"properties": properties,
|
|
161
|
+
"required": required
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def detect_http_method(route_content: str) -> str:
|
|
166
|
+
"""Detect HTTP method from route file."""
|
|
167
|
+
if "export async function GET" in route_content or "export const GET" in route_content:
|
|
168
|
+
return "GET"
|
|
169
|
+
elif "export async function DELETE" in route_content or "export const DELETE" in route_content:
|
|
170
|
+
return "DELETE"
|
|
171
|
+
elif "export async function PUT" in route_content or "export const PUT" in route_content:
|
|
172
|
+
return "PUT"
|
|
173
|
+
elif "export async function PATCH" in route_content or "export const PATCH" in route_content:
|
|
174
|
+
return "PATCH"
|
|
175
|
+
return "POST"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def generate_example_value(prop: dict, field_name: str, variant: str = "default") -> any:
|
|
179
|
+
"""Generate an example value for a property.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
prop: Property definition from schema
|
|
183
|
+
field_name: Name of the field
|
|
184
|
+
variant: Which variant to generate:
|
|
185
|
+
- "default": First/default value
|
|
186
|
+
- "alt": Alternative value (2nd enum, different example)
|
|
187
|
+
- "min": Minimum boundary value
|
|
188
|
+
- "max": Maximum boundary value
|
|
189
|
+
- "empty": Empty/minimal value
|
|
190
|
+
"""
|
|
191
|
+
# Handle enums with variants
|
|
192
|
+
if "enum" in prop:
|
|
193
|
+
enum_values = prop["enum"]
|
|
194
|
+
if variant == "alt" and len(enum_values) > 1:
|
|
195
|
+
return enum_values[1]
|
|
196
|
+
elif variant == "all":
|
|
197
|
+
return enum_values # Return all for reference
|
|
198
|
+
return enum_values[0]
|
|
199
|
+
|
|
200
|
+
if "const" in prop:
|
|
201
|
+
return prop["const"]
|
|
202
|
+
|
|
203
|
+
if variant == "default" and "default" in prop:
|
|
204
|
+
return prop["default"]
|
|
205
|
+
|
|
206
|
+
prop_type = prop.get("type", "string")
|
|
207
|
+
|
|
208
|
+
if prop_type == "string":
|
|
209
|
+
# Handle boundary variants
|
|
210
|
+
min_len = prop.get("minLength", 0)
|
|
211
|
+
max_len = prop.get("maxLength", 100)
|
|
212
|
+
|
|
213
|
+
if variant == "min" and min_len > 0:
|
|
214
|
+
return "x" * min_len
|
|
215
|
+
elif variant == "max" and max_len < 1000:
|
|
216
|
+
return "x" * min(max_len, 50) # Cap at 50 for readability
|
|
217
|
+
elif variant == "empty":
|
|
218
|
+
return ""
|
|
219
|
+
|
|
220
|
+
# Try to generate meaningful example based on field name
|
|
221
|
+
name_lower = field_name.lower()
|
|
222
|
+
if "email" in name_lower:
|
|
223
|
+
return "user@example.com" if variant == "default" else "admin@test.org"
|
|
224
|
+
elif "url" in name_lower or "link" in name_lower:
|
|
225
|
+
return "https://example.com" if variant == "default" else "https://test.org/page"
|
|
226
|
+
elif "domain" in name_lower:
|
|
227
|
+
return "example.com" if variant == "default" else "google.com"
|
|
228
|
+
elif "id" in name_lower:
|
|
229
|
+
return "abc123" if variant == "default" else "xyz789"
|
|
230
|
+
elif "name" in name_lower:
|
|
231
|
+
return "Example Name" if variant == "default" else "Test User"
|
|
232
|
+
elif "model" in name_lower:
|
|
233
|
+
return "gpt-4o" if variant == "default" else "claude-3-opus"
|
|
234
|
+
elif "prompt" in name_lower or "message" in name_lower or "content" in name_lower:
|
|
235
|
+
return "Hello, how can I help you?" if variant == "default" else "Explain quantum computing"
|
|
236
|
+
elif "query" in name_lower or "search" in name_lower:
|
|
237
|
+
return "search term" if variant == "default" else "alternative query"
|
|
238
|
+
else:
|
|
239
|
+
return f"example-{field_name}" if variant == "default" else f"alt-{field_name}"
|
|
240
|
+
|
|
241
|
+
elif prop_type == "number":
|
|
242
|
+
minimum = prop.get("minimum", 0)
|
|
243
|
+
maximum = prop.get("maximum", 1000)
|
|
244
|
+
if variant == "min":
|
|
245
|
+
return minimum
|
|
246
|
+
elif variant == "max":
|
|
247
|
+
return maximum
|
|
248
|
+
elif variant == "alt":
|
|
249
|
+
return (minimum + maximum) // 2 if maximum > minimum else 100
|
|
250
|
+
return prop.get("default", 42)
|
|
251
|
+
|
|
252
|
+
elif prop_type == "integer":
|
|
253
|
+
minimum = prop.get("minimum", 0)
|
|
254
|
+
maximum = prop.get("maximum", 1000)
|
|
255
|
+
if variant == "min":
|
|
256
|
+
return minimum
|
|
257
|
+
elif variant == "max":
|
|
258
|
+
return maximum
|
|
259
|
+
elif variant == "alt":
|
|
260
|
+
return (minimum + maximum) // 2 if maximum > minimum else 50
|
|
261
|
+
return prop.get("default", 100)
|
|
262
|
+
|
|
263
|
+
elif prop_type == "boolean":
|
|
264
|
+
if variant == "alt":
|
|
265
|
+
return not prop.get("default", True)
|
|
266
|
+
return prop.get("default", True)
|
|
267
|
+
|
|
268
|
+
elif prop_type == "array":
|
|
269
|
+
items = prop.get("items", {})
|
|
270
|
+
if variant == "empty":
|
|
271
|
+
return []
|
|
272
|
+
elif variant == "single":
|
|
273
|
+
return [generate_example_value(items, f"{field_name}_item", "default")]
|
|
274
|
+
elif variant == "multiple":
|
|
275
|
+
return [
|
|
276
|
+
generate_example_value(items, f"{field_name}_item", "default"),
|
|
277
|
+
generate_example_value(items, f"{field_name}_item", "alt")
|
|
278
|
+
]
|
|
279
|
+
# Default: show array with one example item
|
|
280
|
+
return [generate_example_value(items, f"{field_name}_item", "default")]
|
|
281
|
+
|
|
282
|
+
elif prop_type == "object":
|
|
283
|
+
# For nested objects, try to generate example if we have properties
|
|
284
|
+
nested_props = prop.get("properties", {})
|
|
285
|
+
if nested_props and variant != "empty":
|
|
286
|
+
result = {}
|
|
287
|
+
for nested_name, nested_prop in nested_props.items():
|
|
288
|
+
result[nested_name] = generate_example_value(nested_prop, nested_name, variant)
|
|
289
|
+
return result
|
|
290
|
+
return {}
|
|
291
|
+
|
|
292
|
+
return "example"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def get_all_enum_fields(properties: dict) -> list:
|
|
296
|
+
"""Find all fields with enum values for generating enum-specific examples."""
|
|
297
|
+
enum_fields = []
|
|
298
|
+
for field_name, prop in properties.items():
|
|
299
|
+
if "enum" in prop and len(prop["enum"]) > 1:
|
|
300
|
+
enum_fields.append({
|
|
301
|
+
"name": field_name,
|
|
302
|
+
"values": prop["enum"]
|
|
303
|
+
})
|
|
304
|
+
return enum_fields
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def generate_curl_examples(endpoint: str, method: str, schema: dict) -> list:
|
|
308
|
+
"""Generate comprehensive curl examples covering all parameter possibilities."""
|
|
309
|
+
examples = []
|
|
310
|
+
properties = schema.get("properties", {})
|
|
311
|
+
required = schema.get("required", [])
|
|
312
|
+
|
|
313
|
+
base_url = f"http://localhost:3001/api/v2/{endpoint}"
|
|
314
|
+
|
|
315
|
+
def make_curl(body: dict, extra_headers: list = None) -> str:
|
|
316
|
+
"""Helper to generate curl command."""
|
|
317
|
+
if method in ["POST", "PUT", "PATCH"]:
|
|
318
|
+
curl = f"curl -X {method} {base_url} \\\n"
|
|
319
|
+
curl += " -H \"Content-Type: application/json\""
|
|
320
|
+
if extra_headers:
|
|
321
|
+
for h in extra_headers:
|
|
322
|
+
curl += f" \\\n -H \"{h}\""
|
|
323
|
+
curl += f" \\\n -d '{json.dumps(body, indent=2)}'"
|
|
324
|
+
else:
|
|
325
|
+
# GET/DELETE - use query params
|
|
326
|
+
if body:
|
|
327
|
+
params = []
|
|
328
|
+
for k, v in body.items():
|
|
329
|
+
if isinstance(v, str):
|
|
330
|
+
params.append(f"{k}={v}")
|
|
331
|
+
else:
|
|
332
|
+
params.append(f"{k}={json.dumps(v)}")
|
|
333
|
+
curl = f"curl \"{base_url}?{'&'.join(params)}\""
|
|
334
|
+
else:
|
|
335
|
+
curl = f"curl {base_url}"
|
|
336
|
+
if extra_headers:
|
|
337
|
+
for h in extra_headers:
|
|
338
|
+
curl += f" \\\n -H \"{h}\""
|
|
339
|
+
return curl
|
|
340
|
+
|
|
341
|
+
# ================================================================
|
|
342
|
+
# Example 1: Minimal (required fields only)
|
|
343
|
+
# ================================================================
|
|
344
|
+
if required:
|
|
345
|
+
minimal_body = {}
|
|
346
|
+
for field in required:
|
|
347
|
+
if field in properties:
|
|
348
|
+
minimal_body[field] = generate_example_value(properties[field], field, "default")
|
|
349
|
+
|
|
350
|
+
if minimal_body:
|
|
351
|
+
examples.append({
|
|
352
|
+
"name": "Minimal (required fields only)",
|
|
353
|
+
"description": f"Request with only required fields: {', '.join(required)}",
|
|
354
|
+
"request": minimal_body,
|
|
355
|
+
"curl": make_curl(minimal_body)
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
# ================================================================
|
|
359
|
+
# Example 2: Full (all parameters)
|
|
360
|
+
# ================================================================
|
|
361
|
+
if properties:
|
|
362
|
+
full_body = {}
|
|
363
|
+
for field, prop in properties.items():
|
|
364
|
+
full_body[field] = generate_example_value(prop, field, "default")
|
|
365
|
+
|
|
366
|
+
examples.append({
|
|
367
|
+
"name": "Full (all parameters)",
|
|
368
|
+
"description": f"Request with all {len(properties)} parameters",
|
|
369
|
+
"request": full_body,
|
|
370
|
+
"curl": make_curl(full_body)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
# ================================================================
|
|
374
|
+
# Example 3: With authentication header
|
|
375
|
+
# ================================================================
|
|
376
|
+
auth_body = {}
|
|
377
|
+
for field in required[:2]: # First 2 required fields
|
|
378
|
+
if field in properties:
|
|
379
|
+
auth_body[field] = generate_example_value(properties[field], field, "default")
|
|
380
|
+
if not auth_body:
|
|
381
|
+
auth_body = {"param": "value"}
|
|
382
|
+
|
|
383
|
+
examples.append({
|
|
384
|
+
"name": "With authentication",
|
|
385
|
+
"description": "Request with X-API-Key header",
|
|
386
|
+
"request": auth_body,
|
|
387
|
+
"curl": make_curl(auth_body, ["X-API-Key: your-api-key"])
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
# ================================================================
|
|
391
|
+
# Example 4-N: Enum variations (one example per enum field per value)
|
|
392
|
+
# ================================================================
|
|
393
|
+
enum_fields = get_all_enum_fields(properties)
|
|
394
|
+
for enum_field in enum_fields:
|
|
395
|
+
field_name = enum_field["name"]
|
|
396
|
+
enum_values = enum_field["values"]
|
|
397
|
+
|
|
398
|
+
for i, enum_val in enumerate(enum_values[:4]): # Cap at 4 values per enum
|
|
399
|
+
enum_body = {}
|
|
400
|
+
# Include required fields
|
|
401
|
+
for field in required:
|
|
402
|
+
if field in properties:
|
|
403
|
+
enum_body[field] = generate_example_value(properties[field], field, "default")
|
|
404
|
+
# Set the enum field to this specific value
|
|
405
|
+
enum_body[field_name] = enum_val
|
|
406
|
+
|
|
407
|
+
examples.append({
|
|
408
|
+
"name": f"{field_name}={enum_val}",
|
|
409
|
+
"description": f"Using {field_name} option: {enum_val}",
|
|
410
|
+
"request": enum_body,
|
|
411
|
+
"curl": make_curl(enum_body)
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
# ================================================================
|
|
415
|
+
# Example: Alternative values
|
|
416
|
+
# ================================================================
|
|
417
|
+
if len(properties) > 1:
|
|
418
|
+
alt_body = {}
|
|
419
|
+
for field, prop in properties.items():
|
|
420
|
+
alt_body[field] = generate_example_value(prop, field, "alt")
|
|
421
|
+
|
|
422
|
+
examples.append({
|
|
423
|
+
"name": "Alternative values",
|
|
424
|
+
"description": "Request with alternative/varied parameter values",
|
|
425
|
+
"request": alt_body,
|
|
426
|
+
"curl": make_curl(alt_body)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
# ================================================================
|
|
430
|
+
# Example: Array fields with multiple items
|
|
431
|
+
# ================================================================
|
|
432
|
+
array_fields = [f for f, p in properties.items() if p.get("type") == "array"]
|
|
433
|
+
if array_fields:
|
|
434
|
+
array_body = {}
|
|
435
|
+
for field in required:
|
|
436
|
+
if field in properties:
|
|
437
|
+
array_body[field] = generate_example_value(properties[field], field, "default")
|
|
438
|
+
|
|
439
|
+
# Show arrays with multiple items
|
|
440
|
+
for field in array_fields:
|
|
441
|
+
array_body[field] = generate_example_value(properties[field], field, "multiple")
|
|
442
|
+
|
|
443
|
+
examples.append({
|
|
444
|
+
"name": "With array items",
|
|
445
|
+
"description": f"Request showing array fields with multiple items: {', '.join(array_fields)}",
|
|
446
|
+
"request": array_body,
|
|
447
|
+
"curl": make_curl(array_body)
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
# ================================================================
|
|
451
|
+
# Example: Boundary values (min/max)
|
|
452
|
+
# ================================================================
|
|
453
|
+
numeric_fields = [f for f, p in properties.items()
|
|
454
|
+
if p.get("type") in ["number", "integer"]
|
|
455
|
+
and (p.get("minimum") is not None or p.get("maximum") is not None)]
|
|
456
|
+
if numeric_fields:
|
|
457
|
+
# Minimum values example
|
|
458
|
+
min_body = {}
|
|
459
|
+
for field in required:
|
|
460
|
+
if field in properties:
|
|
461
|
+
if field in numeric_fields:
|
|
462
|
+
min_body[field] = generate_example_value(properties[field], field, "min")
|
|
463
|
+
else:
|
|
464
|
+
min_body[field] = generate_example_value(properties[field], field, "default")
|
|
465
|
+
for field in numeric_fields:
|
|
466
|
+
min_body[field] = generate_example_value(properties[field], field, "min")
|
|
467
|
+
|
|
468
|
+
examples.append({
|
|
469
|
+
"name": "Minimum boundary values",
|
|
470
|
+
"description": f"Request with minimum values for: {', '.join(numeric_fields)}",
|
|
471
|
+
"request": min_body,
|
|
472
|
+
"curl": make_curl(min_body)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
# Maximum values example
|
|
476
|
+
max_body = {}
|
|
477
|
+
for field in required:
|
|
478
|
+
if field in properties:
|
|
479
|
+
if field in numeric_fields:
|
|
480
|
+
max_body[field] = generate_example_value(properties[field], field, "max")
|
|
481
|
+
else:
|
|
482
|
+
max_body[field] = generate_example_value(properties[field], field, "default")
|
|
483
|
+
for field in numeric_fields:
|
|
484
|
+
max_body[field] = generate_example_value(properties[field], field, "max")
|
|
485
|
+
|
|
486
|
+
examples.append({
|
|
487
|
+
"name": "Maximum boundary values",
|
|
488
|
+
"description": f"Request with maximum values for: {', '.join(numeric_fields)}",
|
|
489
|
+
"request": max_body,
|
|
490
|
+
"curl": make_curl(max_body)
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
# ================================================================
|
|
494
|
+
# Example: Optional fields only (no required - for APIs with all optional)
|
|
495
|
+
# ================================================================
|
|
496
|
+
optional_fields = [f for f in properties.keys() if f not in required]
|
|
497
|
+
if optional_fields and len(optional_fields) > 1:
|
|
498
|
+
optional_body = {}
|
|
499
|
+
for field in optional_fields[:3]: # First 3 optional
|
|
500
|
+
optional_body[field] = generate_example_value(properties[field], field, "default")
|
|
501
|
+
|
|
502
|
+
# Must include required fields too
|
|
503
|
+
for field in required:
|
|
504
|
+
if field in properties:
|
|
505
|
+
optional_body[field] = generate_example_value(properties[field], field, "default")
|
|
506
|
+
|
|
507
|
+
if len(optional_body) != len(properties): # Only if different from full
|
|
508
|
+
examples.append({
|
|
509
|
+
"name": "With optional parameters",
|
|
510
|
+
"description": f"Request including optional fields: {', '.join(optional_fields[:3])}",
|
|
511
|
+
"request": optional_body,
|
|
512
|
+
"curl": make_curl(optional_body)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
return examples
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def generate_test_cases(schema: dict) -> list:
|
|
519
|
+
"""Generate comprehensive test case definitions covering all parameter scenarios."""
|
|
520
|
+
test_cases = []
|
|
521
|
+
properties = schema.get("properties", {})
|
|
522
|
+
required = schema.get("required", [])
|
|
523
|
+
|
|
524
|
+
# Helper to create valid base body
|
|
525
|
+
def make_valid_body():
|
|
526
|
+
body = {}
|
|
527
|
+
for field in required:
|
|
528
|
+
if field in properties:
|
|
529
|
+
body[field] = generate_example_value(properties[field], field, "default")
|
|
530
|
+
return body
|
|
531
|
+
|
|
532
|
+
# ================================================================
|
|
533
|
+
# SUCCESS CASES
|
|
534
|
+
# ================================================================
|
|
535
|
+
|
|
536
|
+
# Test: Valid request with required fields only
|
|
537
|
+
valid_body = make_valid_body()
|
|
538
|
+
if valid_body:
|
|
539
|
+
test_cases.append({
|
|
540
|
+
"name": "Valid request (required only)",
|
|
541
|
+
"description": "Should succeed with valid required parameters",
|
|
542
|
+
"input": valid_body,
|
|
543
|
+
"expectedStatus": 200
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
# Test: Valid request with all fields
|
|
547
|
+
if properties:
|
|
548
|
+
full_body = {}
|
|
549
|
+
for field, prop in properties.items():
|
|
550
|
+
full_body[field] = generate_example_value(prop, field, "default")
|
|
551
|
+
test_cases.append({
|
|
552
|
+
"name": "Valid request (all fields)",
|
|
553
|
+
"description": f"Should succeed with all {len(properties)} parameters",
|
|
554
|
+
"input": full_body,
|
|
555
|
+
"expectedStatus": 200
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
# Test: Valid request with alternative values
|
|
559
|
+
if len(properties) > 1:
|
|
560
|
+
alt_body = {}
|
|
561
|
+
for field, prop in properties.items():
|
|
562
|
+
alt_body[field] = generate_example_value(prop, field, "alt")
|
|
563
|
+
test_cases.append({
|
|
564
|
+
"name": "Valid request (alternative values)",
|
|
565
|
+
"description": "Should succeed with different valid values",
|
|
566
|
+
"input": alt_body,
|
|
567
|
+
"expectedStatus": 200
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
# ================================================================
|
|
571
|
+
# ENUM VALIDATION TESTS
|
|
572
|
+
# ================================================================
|
|
573
|
+
enum_fields = get_all_enum_fields(properties)
|
|
574
|
+
for enum_field in enum_fields:
|
|
575
|
+
field_name = enum_field["name"]
|
|
576
|
+
enum_values = enum_field["values"]
|
|
577
|
+
|
|
578
|
+
# Test: Each valid enum value
|
|
579
|
+
for enum_val in enum_values[:3]: # First 3 values
|
|
580
|
+
enum_body = make_valid_body()
|
|
581
|
+
enum_body[field_name] = enum_val
|
|
582
|
+
test_cases.append({
|
|
583
|
+
"name": f"Valid enum: {field_name}={enum_val}",
|
|
584
|
+
"description": f"Should succeed with {field_name}='{enum_val}'",
|
|
585
|
+
"input": enum_body,
|
|
586
|
+
"expectedStatus": 200
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
# Test: Invalid enum value
|
|
590
|
+
invalid_enum_body = make_valid_body()
|
|
591
|
+
invalid_enum_body[field_name] = "INVALID_ENUM_VALUE_XYZ"
|
|
592
|
+
test_cases.append({
|
|
593
|
+
"name": f"Invalid enum: {field_name}",
|
|
594
|
+
"description": f"Should fail with invalid {field_name} value",
|
|
595
|
+
"input": invalid_enum_body,
|
|
596
|
+
"expectedStatus": 400,
|
|
597
|
+
"expectedError": f"Invalid {field_name}"
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
# ================================================================
|
|
601
|
+
# REQUIRED FIELD TESTS
|
|
602
|
+
# ================================================================
|
|
603
|
+
for req_field in required:
|
|
604
|
+
missing_body = make_valid_body()
|
|
605
|
+
if req_field in missing_body:
|
|
606
|
+
del missing_body[req_field]
|
|
607
|
+
test_cases.append({
|
|
608
|
+
"name": f"Missing required: {req_field}",
|
|
609
|
+
"description": f"Should fail when {req_field} is missing",
|
|
610
|
+
"input": missing_body,
|
|
611
|
+
"expectedStatus": 400,
|
|
612
|
+
"expectedError": f"Required"
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
# ================================================================
|
|
616
|
+
# TYPE VALIDATION TESTS
|
|
617
|
+
# ================================================================
|
|
618
|
+
for field_name, prop in properties.items():
|
|
619
|
+
field_type = prop.get("type", "string")
|
|
620
|
+
invalid_body = make_valid_body()
|
|
621
|
+
|
|
622
|
+
# Generate wrong type based on field type
|
|
623
|
+
if field_type == "string":
|
|
624
|
+
invalid_body[field_name] = 12345 # Number instead of string
|
|
625
|
+
type_desc = "number instead of string"
|
|
626
|
+
elif field_type in ["number", "integer"]:
|
|
627
|
+
invalid_body[field_name] = "not-a-number"
|
|
628
|
+
type_desc = "string instead of number"
|
|
629
|
+
elif field_type == "boolean":
|
|
630
|
+
invalid_body[field_name] = "not-a-boolean"
|
|
631
|
+
type_desc = "string instead of boolean"
|
|
632
|
+
elif field_type == "array":
|
|
633
|
+
invalid_body[field_name] = "not-an-array"
|
|
634
|
+
type_desc = "string instead of array"
|
|
635
|
+
elif field_type == "object":
|
|
636
|
+
invalid_body[field_name] = "not-an-object"
|
|
637
|
+
type_desc = "string instead of object"
|
|
638
|
+
else:
|
|
639
|
+
continue
|
|
640
|
+
|
|
641
|
+
test_cases.append({
|
|
642
|
+
"name": f"Invalid type: {field_name}",
|
|
643
|
+
"description": f"Should fail with {type_desc} for {field_name}",
|
|
644
|
+
"input": invalid_body,
|
|
645
|
+
"expectedStatus": 400
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
# ================================================================
|
|
649
|
+
# BOUNDARY VALUE TESTS
|
|
650
|
+
# ================================================================
|
|
651
|
+
for field_name, prop in properties.items():
|
|
652
|
+
field_type = prop.get("type", "string")
|
|
653
|
+
|
|
654
|
+
# Number/Integer min/max tests
|
|
655
|
+
if field_type in ["number", "integer"]:
|
|
656
|
+
minimum = prop.get("minimum")
|
|
657
|
+
maximum = prop.get("maximum")
|
|
658
|
+
|
|
659
|
+
if minimum is not None:
|
|
660
|
+
# Test at minimum (should pass)
|
|
661
|
+
min_body = make_valid_body()
|
|
662
|
+
min_body[field_name] = minimum
|
|
663
|
+
test_cases.append({
|
|
664
|
+
"name": f"Boundary: {field_name} at minimum ({minimum})",
|
|
665
|
+
"description": f"Should succeed at minimum value",
|
|
666
|
+
"input": min_body,
|
|
667
|
+
"expectedStatus": 200
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
# Test below minimum (should fail)
|
|
671
|
+
below_min_body = make_valid_body()
|
|
672
|
+
below_min_body[field_name] = minimum - 1
|
|
673
|
+
test_cases.append({
|
|
674
|
+
"name": f"Boundary: {field_name} below minimum",
|
|
675
|
+
"description": f"Should fail below minimum ({minimum - 1} < {minimum})",
|
|
676
|
+
"input": below_min_body,
|
|
677
|
+
"expectedStatus": 400
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
if maximum is not None:
|
|
681
|
+
# Test at maximum (should pass)
|
|
682
|
+
max_body = make_valid_body()
|
|
683
|
+
max_body[field_name] = maximum
|
|
684
|
+
test_cases.append({
|
|
685
|
+
"name": f"Boundary: {field_name} at maximum ({maximum})",
|
|
686
|
+
"description": f"Should succeed at maximum value",
|
|
687
|
+
"input": max_body,
|
|
688
|
+
"expectedStatus": 200
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
# Test above maximum (should fail)
|
|
692
|
+
above_max_body = make_valid_body()
|
|
693
|
+
above_max_body[field_name] = maximum + 1
|
|
694
|
+
test_cases.append({
|
|
695
|
+
"name": f"Boundary: {field_name} above maximum",
|
|
696
|
+
"description": f"Should fail above maximum ({maximum + 1} > {maximum})",
|
|
697
|
+
"input": above_max_body,
|
|
698
|
+
"expectedStatus": 400
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
# String length tests
|
|
702
|
+
if field_type == "string":
|
|
703
|
+
min_length = prop.get("minLength")
|
|
704
|
+
max_length = prop.get("maxLength")
|
|
705
|
+
|
|
706
|
+
if min_length is not None and min_length > 0:
|
|
707
|
+
# Test below minLength
|
|
708
|
+
short_body = make_valid_body()
|
|
709
|
+
short_body[field_name] = "x" * (min_length - 1) if min_length > 1 else ""
|
|
710
|
+
test_cases.append({
|
|
711
|
+
"name": f"Boundary: {field_name} too short",
|
|
712
|
+
"description": f"Should fail when {field_name} < {min_length} chars",
|
|
713
|
+
"input": short_body,
|
|
714
|
+
"expectedStatus": 400
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
if max_length is not None:
|
|
718
|
+
# Test above maxLength
|
|
719
|
+
long_body = make_valid_body()
|
|
720
|
+
long_body[field_name] = "x" * (max_length + 1)
|
|
721
|
+
test_cases.append({
|
|
722
|
+
"name": f"Boundary: {field_name} too long",
|
|
723
|
+
"description": f"Should fail when {field_name} > {max_length} chars",
|
|
724
|
+
"input": long_body,
|
|
725
|
+
"expectedStatus": 400
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
# ================================================================
|
|
729
|
+
# ARRAY TESTS
|
|
730
|
+
# ================================================================
|
|
731
|
+
array_fields = [(f, p) for f, p in properties.items() if p.get("type") == "array"]
|
|
732
|
+
for field_name, prop in array_fields:
|
|
733
|
+
# Empty array (if allowed)
|
|
734
|
+
empty_array_body = make_valid_body()
|
|
735
|
+
empty_array_body[field_name] = []
|
|
736
|
+
test_cases.append({
|
|
737
|
+
"name": f"Array: {field_name} empty",
|
|
738
|
+
"description": f"Test with empty {field_name} array",
|
|
739
|
+
"input": empty_array_body,
|
|
740
|
+
"expectedStatus": 200 # Usually allowed unless minItems
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
# Array with multiple items
|
|
744
|
+
multi_array_body = make_valid_body()
|
|
745
|
+
multi_array_body[field_name] = generate_example_value(prop, field_name, "multiple")
|
|
746
|
+
test_cases.append({
|
|
747
|
+
"name": f"Array: {field_name} multiple items",
|
|
748
|
+
"description": f"Test with multiple items in {field_name}",
|
|
749
|
+
"input": multi_array_body,
|
|
750
|
+
"expectedStatus": 200
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
# ================================================================
|
|
754
|
+
# EDGE CASES
|
|
755
|
+
# ================================================================
|
|
756
|
+
|
|
757
|
+
# Empty body
|
|
758
|
+
test_cases.append({
|
|
759
|
+
"name": "Empty body",
|
|
760
|
+
"description": "Should fail with empty request body",
|
|
761
|
+
"input": {},
|
|
762
|
+
"expectedStatus": 400
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
# Null values for required fields
|
|
766
|
+
for req_field in required[:2]: # First 2 required
|
|
767
|
+
null_body = make_valid_body()
|
|
768
|
+
null_body[req_field] = None
|
|
769
|
+
test_cases.append({
|
|
770
|
+
"name": f"Null value: {req_field}",
|
|
771
|
+
"description": f"Should fail when {req_field} is null",
|
|
772
|
+
"input": null_body,
|
|
773
|
+
"expectedStatus": 400
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
# Extra unknown field (should be ignored or error depending on strictness)
|
|
777
|
+
extra_field_body = make_valid_body()
|
|
778
|
+
extra_field_body["unknownExtraField123"] = "should-be-ignored"
|
|
779
|
+
test_cases.append({
|
|
780
|
+
"name": "Extra unknown field",
|
|
781
|
+
"description": "Test behavior with unexpected field",
|
|
782
|
+
"input": extra_field_body,
|
|
783
|
+
"expectedStatus": 200, # Most APIs ignore extra fields
|
|
784
|
+
"note": "Depends on schema strictness"
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
return test_cases
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def generate_orchestration_examples(endpoint: str, combine_config: dict, method: str) -> list:
|
|
791
|
+
"""Generate orchestration examples for combined API endpoints.
|
|
792
|
+
|
|
793
|
+
Shows how multiple APIs are called in sequence/parallel and how data flows.
|
|
794
|
+
"""
|
|
795
|
+
examples = []
|
|
796
|
+
source_elements = combine_config.get("source_elements", [])
|
|
797
|
+
flow_type = combine_config.get("flow_type", "sequential")
|
|
798
|
+
error_strategy = combine_config.get("error_strategy", "fail-fast")
|
|
799
|
+
|
|
800
|
+
if len(source_elements) < 2:
|
|
801
|
+
return examples
|
|
802
|
+
|
|
803
|
+
base_url = f"http://localhost:3001/api/v2/{endpoint}"
|
|
804
|
+
|
|
805
|
+
# Get source API names
|
|
806
|
+
source_names = []
|
|
807
|
+
for elem in source_elements:
|
|
808
|
+
if isinstance(elem, dict):
|
|
809
|
+
source_names.append(elem.get("name", "unknown"))
|
|
810
|
+
else:
|
|
811
|
+
source_names.append(str(elem))
|
|
812
|
+
|
|
813
|
+
# ================================================================
|
|
814
|
+
# Example 1: Sequential flow
|
|
815
|
+
# ================================================================
|
|
816
|
+
if flow_type == "sequential":
|
|
817
|
+
seq_body = {
|
|
818
|
+
"steps": [
|
|
819
|
+
{"api": source_names[0], "params": {"input": "example"}},
|
|
820
|
+
{"api": source_names[1], "params": {"useResultFrom": source_names[0]}}
|
|
821
|
+
]
|
|
822
|
+
}
|
|
823
|
+
curl = f'curl -X {method} {base_url} \\\n'
|
|
824
|
+
curl += ' -H "Content-Type: application/json" \\\n'
|
|
825
|
+
curl += f" -d '{json.dumps(seq_body, indent=2)}'"
|
|
826
|
+
|
|
827
|
+
examples.append({
|
|
828
|
+
"name": "Sequential orchestration",
|
|
829
|
+
"description": f"Calls {source_names[0]} first, then {source_names[1]} with results",
|
|
830
|
+
"request": seq_body,
|
|
831
|
+
"curl": curl,
|
|
832
|
+
"flow": f"{source_names[0]} → {source_names[1]}"
|
|
833
|
+
})
|
|
834
|
+
|
|
835
|
+
# ================================================================
|
|
836
|
+
# Example 2: Parallel flow
|
|
837
|
+
# ================================================================
|
|
838
|
+
elif flow_type == "parallel":
|
|
839
|
+
par_body = {
|
|
840
|
+
"parallel": True,
|
|
841
|
+
"apis": [
|
|
842
|
+
{"name": source_names[0], "params": {"query": "example1"}},
|
|
843
|
+
{"name": source_names[1], "params": {"query": "example2"}}
|
|
844
|
+
],
|
|
845
|
+
"merge_strategy": "combine"
|
|
846
|
+
}
|
|
847
|
+
curl = f'curl -X {method} {base_url} \\\n'
|
|
848
|
+
curl += ' -H "Content-Type: application/json" \\\n'
|
|
849
|
+
curl += f" -d '{json.dumps(par_body, indent=2)}'"
|
|
850
|
+
|
|
851
|
+
examples.append({
|
|
852
|
+
"name": "Parallel orchestration",
|
|
853
|
+
"description": f"Calls {source_names[0]} and {source_names[1]} simultaneously",
|
|
854
|
+
"request": par_body,
|
|
855
|
+
"curl": curl,
|
|
856
|
+
"flow": f"[{source_names[0]} || {source_names[1]}] → merge"
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
# ================================================================
|
|
860
|
+
# Example 3: Conditional flow
|
|
861
|
+
# ================================================================
|
|
862
|
+
elif flow_type == "conditional":
|
|
863
|
+
cond_body = {
|
|
864
|
+
"condition": {
|
|
865
|
+
"check": source_names[0],
|
|
866
|
+
"if_success": source_names[1],
|
|
867
|
+
"if_failure": "fallback"
|
|
868
|
+
},
|
|
869
|
+
"params": {"input": "example"}
|
|
870
|
+
}
|
|
871
|
+
curl = f'curl -X {method} {base_url} \\\n'
|
|
872
|
+
curl += ' -H "Content-Type: application/json" \\\n'
|
|
873
|
+
curl += f" -d '{json.dumps(cond_body, indent=2)}'"
|
|
874
|
+
|
|
875
|
+
examples.append({
|
|
876
|
+
"name": "Conditional orchestration",
|
|
877
|
+
"description": f"Calls {source_names[1]} only if {source_names[0]} succeeds",
|
|
878
|
+
"request": cond_body,
|
|
879
|
+
"curl": curl,
|
|
880
|
+
"flow": f"{source_names[0]} ? {source_names[1]} : fallback"
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
# ================================================================
|
|
884
|
+
# Example 4: With error handling
|
|
885
|
+
# ================================================================
|
|
886
|
+
error_body = {
|
|
887
|
+
"steps": [
|
|
888
|
+
{"api": source_names[0], "params": {"input": "data"}},
|
|
889
|
+
{"api": source_names[1], "params": {"input": "data"}}
|
|
890
|
+
],
|
|
891
|
+
"error_strategy": error_strategy,
|
|
892
|
+
"retry": {"attempts": 3, "delay_ms": 1000}
|
|
893
|
+
}
|
|
894
|
+
curl = f'curl -X {method} {base_url} \\\n'
|
|
895
|
+
curl += ' -H "Content-Type: application/json" \\\n'
|
|
896
|
+
curl += f" -d '{json.dumps(error_body, indent=2)}'"
|
|
897
|
+
|
|
898
|
+
examples.append({
|
|
899
|
+
"name": f"With {error_strategy} error handling",
|
|
900
|
+
"description": f"Orchestration with {error_strategy} strategy and retry logic",
|
|
901
|
+
"request": error_body,
|
|
902
|
+
"curl": curl
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
# ================================================================
|
|
906
|
+
# Example 5: Full data flow
|
|
907
|
+
# ================================================================
|
|
908
|
+
full_body = {
|
|
909
|
+
"input": {
|
|
910
|
+
"source": "user-provided",
|
|
911
|
+
"data": {"query": "example query", "options": {"format": "json"}}
|
|
912
|
+
},
|
|
913
|
+
"flow": [
|
|
914
|
+
{
|
|
915
|
+
"step": 1,
|
|
916
|
+
"api": source_names[0],
|
|
917
|
+
"transform": "extract.content"
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
"step": 2,
|
|
921
|
+
"api": source_names[1],
|
|
922
|
+
"input_mapping": {"content": "$.step1.result"},
|
|
923
|
+
"transform": "format.output"
|
|
924
|
+
}
|
|
925
|
+
],
|
|
926
|
+
"output": {"include": ["final_result", "metadata"]}
|
|
927
|
+
}
|
|
928
|
+
curl = f'curl -X {method} {base_url} \\\n'
|
|
929
|
+
curl += ' -H "Content-Type: application/json" \\\n'
|
|
930
|
+
curl += f" -d '{json.dumps(full_body, indent=2)}'"
|
|
931
|
+
|
|
932
|
+
examples.append({
|
|
933
|
+
"name": "Complete orchestration with transforms",
|
|
934
|
+
"description": "Full data flow with input mapping and output transformation",
|
|
935
|
+
"request": full_body,
|
|
936
|
+
"curl": curl,
|
|
937
|
+
"flow_diagram": f"""
|
|
938
|
+
Input → [{source_names[0]}] → transform → [{source_names[1]}] → Output
|
|
939
|
+
↑ ↑
|
|
940
|
+
extract.content format.output
|
|
941
|
+
"""
|
|
942
|
+
})
|
|
943
|
+
|
|
944
|
+
return examples
|
|
945
|
+
|
|
946
|
+
|
|
947
|
+
def generate_manifest_entry(endpoint: str, endpoint_data: dict, state: dict) -> dict:
|
|
948
|
+
"""Generate a complete manifest entry for the endpoint."""
|
|
949
|
+
# Check if this is a combined workflow
|
|
950
|
+
combine_config = state.get("combine_config", {})
|
|
951
|
+
is_combined = bool(combine_config.get("source_elements"))
|
|
952
|
+
|
|
953
|
+
# Find schema file
|
|
954
|
+
schema_file = endpoint_data.get("phases", {}).get("schema_creation", {}).get("schema_file")
|
|
955
|
+
if not schema_file:
|
|
956
|
+
# Try to find it
|
|
957
|
+
schema_file = f"src/lib/schemas/{endpoint}.ts"
|
|
958
|
+
|
|
959
|
+
schema_path = STATE_FILE.parent.parent / schema_file
|
|
960
|
+
schema_content = ""
|
|
961
|
+
if schema_path.exists():
|
|
962
|
+
schema_content = schema_path.read_text()
|
|
963
|
+
|
|
964
|
+
# Find route file
|
|
965
|
+
route_content = ""
|
|
966
|
+
files_created = endpoint_data.get("files_created", []) or state.get("files_created", [])
|
|
967
|
+
for f in files_created:
|
|
968
|
+
if "route.ts" in f:
|
|
969
|
+
route_path = STATE_FILE.parent.parent / f
|
|
970
|
+
if route_path.exists():
|
|
971
|
+
route_content = route_path.read_text()
|
|
972
|
+
break
|
|
973
|
+
|
|
974
|
+
# Parse schema
|
|
975
|
+
request_schema = parse_zod_schema(schema_content) if schema_content else {"type": "object", "properties": {}}
|
|
976
|
+
|
|
977
|
+
# Detect method
|
|
978
|
+
method = detect_http_method(route_content)
|
|
979
|
+
|
|
980
|
+
# Generate examples
|
|
981
|
+
examples = generate_curl_examples(endpoint, method, request_schema)
|
|
982
|
+
|
|
983
|
+
# Add orchestration examples for combined endpoints
|
|
984
|
+
if is_combined:
|
|
985
|
+
orchestration_examples = generate_orchestration_examples(endpoint, combine_config, method)
|
|
986
|
+
examples.extend(orchestration_examples)
|
|
987
|
+
|
|
988
|
+
# Generate test cases
|
|
989
|
+
test_cases = generate_test_cases(request_schema)
|
|
990
|
+
|
|
991
|
+
# Get interview decisions for description
|
|
992
|
+
interview = endpoint_data.get("phases", {}).get("interview", {})
|
|
993
|
+
decisions = interview.get("decisions", {})
|
|
994
|
+
questions = interview.get("questions", [])
|
|
995
|
+
|
|
996
|
+
# Build description from interview
|
|
997
|
+
description = f"API endpoint for {endpoint}"
|
|
998
|
+
if decisions:
|
|
999
|
+
decision_summary = ", ".join(f"{k}: {v}" for k, v in list(decisions.items())[:3])
|
|
1000
|
+
description = f"{endpoint} endpoint. Configured for: {decision_summary}"
|
|
1001
|
+
|
|
1002
|
+
# Build the manifest entry
|
|
1003
|
+
entry = {
|
|
1004
|
+
"id": f"{endpoint}-{method.lower()}",
|
|
1005
|
+
"method": method,
|
|
1006
|
+
"path": f"/api/v2/{endpoint}",
|
|
1007
|
+
"summary": f"{method} {endpoint}",
|
|
1008
|
+
"description": description,
|
|
1009
|
+
"requestSchema": request_schema,
|
|
1010
|
+
"responseSchema": {
|
|
1011
|
+
"type": "object",
|
|
1012
|
+
"properties": {
|
|
1013
|
+
"success": {"type": "boolean"},
|
|
1014
|
+
"data": {"type": "object"},
|
|
1015
|
+
"error": {"type": "string"}
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
"examples": examples,
|
|
1019
|
+
"testCases": test_cases,
|
|
1020
|
+
"metadata": {
|
|
1021
|
+
"generatedAt": datetime.now().isoformat(),
|
|
1022
|
+
"generatedBy": "api-dev-tools v3.10.0",
|
|
1023
|
+
"schemaFile": schema_file,
|
|
1024
|
+
"researchFresh": True
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
# Add combined workflow metadata
|
|
1029
|
+
if is_combined:
|
|
1030
|
+
source_names = []
|
|
1031
|
+
for elem in combine_config.get("source_elements", []):
|
|
1032
|
+
if isinstance(elem, dict):
|
|
1033
|
+
source_names.append(elem.get("name", "unknown"))
|
|
1034
|
+
else:
|
|
1035
|
+
source_names.append(str(elem))
|
|
1036
|
+
|
|
1037
|
+
entry["metadata"]["isCombined"] = True
|
|
1038
|
+
entry["metadata"]["sourceApis"] = source_names
|
|
1039
|
+
entry["metadata"]["flowType"] = combine_config.get("flow_type", "sequential")
|
|
1040
|
+
entry["metadata"]["errorStrategy"] = combine_config.get("error_strategy", "fail-fast")
|
|
1041
|
+
|
|
1042
|
+
return entry
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
def update_manifest(entry: dict, manifest_path: Path = None):
|
|
1046
|
+
"""Add or update entry in api-tests-manifest.json."""
|
|
1047
|
+
if manifest_path is None:
|
|
1048
|
+
manifest_path = DEFAULT_MANIFEST
|
|
1049
|
+
|
|
1050
|
+
if not manifest_path.exists():
|
|
1051
|
+
# Create minimal manifest structure
|
|
1052
|
+
manifest = {
|
|
1053
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
1054
|
+
"version": "2.0.0",
|
|
1055
|
+
"lastUpdated": datetime.now().strftime("%Y-%m-%d"),
|
|
1056
|
+
"baseUrl": "http://localhost:3001",
|
|
1057
|
+
"sections": []
|
|
1058
|
+
}
|
|
1059
|
+
else:
|
|
1060
|
+
manifest = json.loads(manifest_path.read_text())
|
|
1061
|
+
|
|
1062
|
+
# Find or create "Generated APIs" section
|
|
1063
|
+
generated_section = None
|
|
1064
|
+
for section in manifest.get("sections", []):
|
|
1065
|
+
if section.get("id") == "generated-apis":
|
|
1066
|
+
generated_section = section
|
|
1067
|
+
break
|
|
1068
|
+
|
|
1069
|
+
if not generated_section:
|
|
1070
|
+
generated_section = {
|
|
1071
|
+
"id": "generated-apis",
|
|
1072
|
+
"name": "Generated APIs",
|
|
1073
|
+
"description": "APIs created with /api-create workflow",
|
|
1074
|
+
"endpoints": []
|
|
1075
|
+
}
|
|
1076
|
+
manifest.setdefault("sections", []).append(generated_section)
|
|
1077
|
+
|
|
1078
|
+
# Remove existing entry with same ID
|
|
1079
|
+
generated_section["endpoints"] = [
|
|
1080
|
+
e for e in generated_section.get("endpoints", [])
|
|
1081
|
+
if e.get("id") != entry["id"]
|
|
1082
|
+
]
|
|
1083
|
+
|
|
1084
|
+
# Add new entry
|
|
1085
|
+
generated_section["endpoints"].append(entry)
|
|
1086
|
+
|
|
1087
|
+
# Update timestamp
|
|
1088
|
+
manifest["lastUpdated"] = datetime.now().strftime("%Y-%m-%d")
|
|
1089
|
+
|
|
1090
|
+
# Write back
|
|
1091
|
+
manifest_path.write_text(json.dumps(manifest, indent=2))
|
|
1092
|
+
return True
|
|
1093
|
+
|
|
1094
|
+
|
|
1095
|
+
def main():
|
|
1096
|
+
try:
|
|
1097
|
+
input_data = json.load(sys.stdin)
|
|
1098
|
+
except json.JSONDecodeError:
|
|
1099
|
+
input_data = {}
|
|
1100
|
+
|
|
1101
|
+
# Load state
|
|
1102
|
+
if not STATE_FILE.exists():
|
|
1103
|
+
print(json.dumps({"continue": True}))
|
|
1104
|
+
sys.exit(0)
|
|
1105
|
+
|
|
1106
|
+
try:
|
|
1107
|
+
state = json.loads(STATE_FILE.read_text())
|
|
1108
|
+
except json.JSONDecodeError:
|
|
1109
|
+
print(json.dumps({"continue": True}))
|
|
1110
|
+
sys.exit(0)
|
|
1111
|
+
|
|
1112
|
+
endpoint, endpoint_data = get_active_endpoint(state)
|
|
1113
|
+
if not endpoint or not endpoint_data:
|
|
1114
|
+
print(json.dumps({"continue": True}))
|
|
1115
|
+
sys.exit(0)
|
|
1116
|
+
|
|
1117
|
+
# Check if documentation phase is complete or completing
|
|
1118
|
+
doc_phase = endpoint_data.get("phases", {}).get("documentation", {})
|
|
1119
|
+
if doc_phase.get("status") not in ["in_progress", "complete"]:
|
|
1120
|
+
print(json.dumps({"continue": True}))
|
|
1121
|
+
sys.exit(0)
|
|
1122
|
+
|
|
1123
|
+
# Check if manifest already updated
|
|
1124
|
+
if doc_phase.get("manifest_updated"):
|
|
1125
|
+
print(json.dumps({"continue": True}))
|
|
1126
|
+
sys.exit(0)
|
|
1127
|
+
|
|
1128
|
+
# Generate manifest entry
|
|
1129
|
+
try:
|
|
1130
|
+
entry = generate_manifest_entry(endpoint, endpoint_data, state)
|
|
1131
|
+
|
|
1132
|
+
# Update manifest file
|
|
1133
|
+
manifest_path = STATE_FILE.parent.parent / "src" / "app" / "api-test" / "api-tests-manifest.json"
|
|
1134
|
+
if manifest_path.exists():
|
|
1135
|
+
update_manifest(entry, manifest_path)
|
|
1136
|
+
|
|
1137
|
+
# Update state to mark manifest as updated
|
|
1138
|
+
doc_phase["manifest_updated"] = True
|
|
1139
|
+
doc_phase["manifest_entry_id"] = entry["id"]
|
|
1140
|
+
STATE_FILE.write_text(json.dumps(state, indent=2))
|
|
1141
|
+
|
|
1142
|
+
print(json.dumps({
|
|
1143
|
+
"continue": True,
|
|
1144
|
+
"message": f"Generated manifest entry: {entry['id']} with {len(entry['examples'])} examples and {len(entry['testCases'])} test cases"
|
|
1145
|
+
}))
|
|
1146
|
+
else:
|
|
1147
|
+
print(json.dumps({
|
|
1148
|
+
"continue": True,
|
|
1149
|
+
"message": f"Manifest file not found at {manifest_path}"
|
|
1150
|
+
}))
|
|
1151
|
+
except Exception as e:
|
|
1152
|
+
print(json.dumps({
|
|
1153
|
+
"continue": True,
|
|
1154
|
+
"message": f"Error generating manifest: {str(e)}"
|
|
1155
|
+
}))
|
|
1156
|
+
|
|
1157
|
+
sys.exit(0)
|
|
1158
|
+
|
|
1159
|
+
|
|
1160
|
+
if __name__ == "__main__":
|
|
1161
|
+
main()
|