@voodocs/cli 0.1.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/LICENSE +37 -0
- package/README.md +153 -0
- package/USAGE.md +314 -0
- package/cli.py +1340 -0
- package/examples/.cursorrules +437 -0
- package/examples/instructions/.claude/instructions.md +372 -0
- package/examples/instructions/.cursorrules +437 -0
- package/examples/instructions/.windsurfrules +437 -0
- package/examples/instructions/VOODOCS_INSTRUCTIONS.md +437 -0
- package/examples/math_example.py +41 -0
- package/examples/phase2_test.py +24 -0
- package/examples/test_compound_conditions.py +40 -0
- package/examples/test_math_example.py +186 -0
- package/lib/darkarts/README.md +115 -0
- package/lib/darkarts/__init__.py +16 -0
- package/lib/darkarts/annotations/__init__.py +34 -0
- package/lib/darkarts/annotations/parser.py +618 -0
- package/lib/darkarts/annotations/types.py +181 -0
- package/lib/darkarts/cli.py +128 -0
- package/lib/darkarts/core/__init__.py +32 -0
- package/lib/darkarts/core/interface.py +256 -0
- package/lib/darkarts/core/loader.py +231 -0
- package/lib/darkarts/core/plugin.py +215 -0
- package/lib/darkarts/core/registry.py +146 -0
- package/lib/darkarts/exceptions.py +51 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts +9 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js +69 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts +111 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js +365 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js.map +1 -0
- package/lib/darkarts/parsers/typescript/package-lock.json +51 -0
- package/lib/darkarts/parsers/typescript/package.json +19 -0
- package/lib/darkarts/parsers/typescript/src/cli.ts +41 -0
- package/lib/darkarts/parsers/typescript/src/parser.ts +408 -0
- package/lib/darkarts/parsers/typescript/tsconfig.json +19 -0
- package/lib/darkarts/plugins/voodocs/__init__.py +379 -0
- package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +151 -0
- package/lib/darkarts/plugins/voodocs/annotation_validator.py +280 -0
- package/lib/darkarts/plugins/voodocs/api_spec_generator.py +486 -0
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +610 -0
- package/lib/darkarts/plugins/voodocs/html_exporter.py +260 -0
- package/lib/darkarts/plugins/voodocs/instruction_generator.py +706 -0
- package/lib/darkarts/plugins/voodocs/pdf_exporter.py +66 -0
- package/lib/darkarts/plugins/voodocs/test_generator.py +636 -0
- package/package.json +70 -0
- package/requirements.txt +13 -0
- package/templates/ci/github-actions.yml +73 -0
- package/templates/ci/gitlab-ci.yml +35 -0
- package/templates/ci/pre-commit-hook.sh +26 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DarkArts API Specification Generator
|
|
3
|
+
|
|
4
|
+
Generates OpenAPI/Swagger specifications from DarkArts annotations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import List, Optional, Dict, Any
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import json
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from darkarts.annotations.types import (
|
|
13
|
+
ParsedAnnotations,
|
|
14
|
+
FunctionAnnotation,
|
|
15
|
+
ClassAnnotation,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class APISpecGenerator:
|
|
20
|
+
"""Generates API specifications from DarkArts annotations."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, format: str = "openapi"):
|
|
23
|
+
"""
|
|
24
|
+
Initialize API spec generator.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
format: Specification format ("openapi", "swagger", "graphql")
|
|
28
|
+
"""
|
|
29
|
+
self.format = format
|
|
30
|
+
|
|
31
|
+
def generate(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
|
|
32
|
+
"""Generate API specification from parsed annotations."""
|
|
33
|
+
if self.format == "openapi":
|
|
34
|
+
return self._generate_openapi(parsed, output_file)
|
|
35
|
+
elif self.format == "swagger":
|
|
36
|
+
return self._generate_openapi(parsed, output_file) # Same as OpenAPI
|
|
37
|
+
elif self.format == "graphql":
|
|
38
|
+
return self._generate_graphql(parsed, output_file)
|
|
39
|
+
else:
|
|
40
|
+
raise ValueError(f"Unsupported format: {self.format}")
|
|
41
|
+
|
|
42
|
+
def _generate_openapi(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
|
|
43
|
+
"""Generate OpenAPI 3.0 specification."""
|
|
44
|
+
spec = {
|
|
45
|
+
"openapi": "3.0.0",
|
|
46
|
+
"info": {
|
|
47
|
+
"title": parsed.module.name.replace("_", " ").title() + " API",
|
|
48
|
+
"version": "1.0.0",
|
|
49
|
+
"description": parsed.module.module_purpose or "API generated from DarkArts annotations"
|
|
50
|
+
},
|
|
51
|
+
"paths": {},
|
|
52
|
+
"components": {
|
|
53
|
+
"schemas": {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Add dependencies as external docs
|
|
58
|
+
if parsed.module.dependencies:
|
|
59
|
+
spec["externalDocs"] = {
|
|
60
|
+
"description": "Dependencies",
|
|
61
|
+
"url": "#"
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Generate paths from functions
|
|
65
|
+
all_functions = parsed.get_all_functions()
|
|
66
|
+
for func in all_functions:
|
|
67
|
+
path = f"/{func.name}"
|
|
68
|
+
spec["paths"][path] = self._generate_openapi_path(func)
|
|
69
|
+
|
|
70
|
+
# Generate schemas from classes
|
|
71
|
+
for cls in parsed.module.classes:
|
|
72
|
+
spec["components"]["schemas"][cls.name] = self._generate_openapi_schema(cls)
|
|
73
|
+
|
|
74
|
+
# Convert to YAML (more readable than JSON for API specs)
|
|
75
|
+
spec_yaml = yaml.dump(spec, default_flow_style=False, sort_keys=False)
|
|
76
|
+
|
|
77
|
+
if output_file:
|
|
78
|
+
output_path = Path(output_file)
|
|
79
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
|
|
81
|
+
if output_file.endswith('.json'):
|
|
82
|
+
output_path.write_text(json.dumps(spec, indent=2), encoding='utf-8')
|
|
83
|
+
else:
|
|
84
|
+
output_path.write_text(spec_yaml, encoding='utf-8')
|
|
85
|
+
|
|
86
|
+
return spec_yaml
|
|
87
|
+
|
|
88
|
+
def _generate_openapi_path(self, func: FunctionAnnotation) -> Dict[str, Any]:
|
|
89
|
+
"""Generate OpenAPI path item for a function."""
|
|
90
|
+
operation = {
|
|
91
|
+
"post": {
|
|
92
|
+
"summary": func.solve or f"Execute {func.name}",
|
|
93
|
+
"description": self._build_function_description(func),
|
|
94
|
+
"requestBody": {
|
|
95
|
+
"required": True,
|
|
96
|
+
"content": {
|
|
97
|
+
"application/json": {
|
|
98
|
+
"schema": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": self._extract_parameters_from_preconditions(func)
|
|
101
|
+
},
|
|
102
|
+
"examples": {
|
|
103
|
+
"example1": {
|
|
104
|
+
"summary": "Example request",
|
|
105
|
+
"value": self._generate_request_example(func)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"responses": {
|
|
112
|
+
"200": {
|
|
113
|
+
"description": "Successful operation",
|
|
114
|
+
"content": {
|
|
115
|
+
"application/json": {
|
|
116
|
+
"schema": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": self._extract_response_from_postconditions(func)
|
|
119
|
+
},
|
|
120
|
+
"examples": {
|
|
121
|
+
"example1": {
|
|
122
|
+
"summary": "Example response",
|
|
123
|
+
"value": self._generate_response_example(func)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
# Add error responses
|
|
134
|
+
if func.error_cases:
|
|
135
|
+
for error in func.error_cases:
|
|
136
|
+
error_str = str(error)
|
|
137
|
+
if '→' in error_str:
|
|
138
|
+
condition, error_type = error_str.split('→')
|
|
139
|
+
error_type = error_type.strip()
|
|
140
|
+
|
|
141
|
+
# Map error types to HTTP status codes
|
|
142
|
+
status_code = self._map_error_to_status_code(error_type)
|
|
143
|
+
operation["post"]["responses"][str(status_code)] = {
|
|
144
|
+
"description": error_type,
|
|
145
|
+
"content": {
|
|
146
|
+
"application/json": {
|
|
147
|
+
"schema": {
|
|
148
|
+
"type": "object",
|
|
149
|
+
"properties": {
|
|
150
|
+
"error": {"type": "string"},
|
|
151
|
+
"message": {"type": "string"}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Add performance info
|
|
159
|
+
if func.complexity:
|
|
160
|
+
operation["post"]["x-complexity"] = {
|
|
161
|
+
"time": func.complexity.time,
|
|
162
|
+
"space": func.complexity.space
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if func.optimize:
|
|
166
|
+
operation["post"]["x-optimization"] = func.optimize
|
|
167
|
+
|
|
168
|
+
return operation
|
|
169
|
+
|
|
170
|
+
def _generate_openapi_schema(self, cls: ClassAnnotation) -> Dict[str, Any]:
|
|
171
|
+
"""Generate OpenAPI schema for a class."""
|
|
172
|
+
schema = {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"description": f"{cls.name} class",
|
|
175
|
+
"properties": {},
|
|
176
|
+
"x-invariants": cls.class_invariants or []
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if cls.state_transitions:
|
|
180
|
+
schema["x-state-transitions"] = [str(st) for st in cls.state_transitions]
|
|
181
|
+
|
|
182
|
+
# Add methods as operations
|
|
183
|
+
if cls.methods:
|
|
184
|
+
schema["x-methods"] = [
|
|
185
|
+
{
|
|
186
|
+
"name": method.name,
|
|
187
|
+
"description": method.solve or "",
|
|
188
|
+
"preconditions": method.preconditions or [],
|
|
189
|
+
"postconditions": method.postconditions or []
|
|
190
|
+
}
|
|
191
|
+
for method in cls.methods
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
return schema
|
|
195
|
+
|
|
196
|
+
def _build_function_description(self, func: FunctionAnnotation) -> str:
|
|
197
|
+
"""Build detailed description for a function."""
|
|
198
|
+
parts = []
|
|
199
|
+
|
|
200
|
+
if func.solve:
|
|
201
|
+
parts.append(func.solve)
|
|
202
|
+
|
|
203
|
+
if func.preconditions:
|
|
204
|
+
parts.append("\n\n**Preconditions:**")
|
|
205
|
+
for pre in func.preconditions:
|
|
206
|
+
parts.append(f"- {pre}")
|
|
207
|
+
|
|
208
|
+
if func.postconditions:
|
|
209
|
+
parts.append("\n\n**Postconditions:**")
|
|
210
|
+
for post in func.postconditions:
|
|
211
|
+
parts.append(f"- {post}")
|
|
212
|
+
|
|
213
|
+
if func.invariants:
|
|
214
|
+
parts.append("\n\n**Invariants:**")
|
|
215
|
+
for inv in func.invariants:
|
|
216
|
+
parts.append(f"- {inv}")
|
|
217
|
+
|
|
218
|
+
return "\n".join(parts)
|
|
219
|
+
|
|
220
|
+
def _extract_parameters_from_preconditions(self, func: FunctionAnnotation) -> Dict[str, Any]:
|
|
221
|
+
"""Extract parameter definitions from preconditions with enhanced type inference."""
|
|
222
|
+
import re
|
|
223
|
+
params = {}
|
|
224
|
+
|
|
225
|
+
if func.preconditions:
|
|
226
|
+
for pre in func.preconditions:
|
|
227
|
+
# Extract parameter name and infer type
|
|
228
|
+
param_info = self._infer_param_from_condition(pre)
|
|
229
|
+
if param_info:
|
|
230
|
+
param_name, param_schema = param_info
|
|
231
|
+
if param_name not in params:
|
|
232
|
+
params[param_name] = param_schema
|
|
233
|
+
else:
|
|
234
|
+
# Merge constraints
|
|
235
|
+
if 'minimum' in param_schema and 'minimum' not in params[param_name]:
|
|
236
|
+
params[param_name]['minimum'] = param_schema['minimum']
|
|
237
|
+
if 'maximum' in param_schema and 'maximum' not in params[param_name]:
|
|
238
|
+
params[param_name]['maximum'] = param_schema['maximum']
|
|
239
|
+
if 'enum' in param_schema:
|
|
240
|
+
params[param_name]['enum'] = param_schema['enum']
|
|
241
|
+
|
|
242
|
+
# If no parameters extracted, add a generic one
|
|
243
|
+
if not params:
|
|
244
|
+
params["input"] = {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"description": "Input parameters"
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return params
|
|
250
|
+
|
|
251
|
+
def _infer_param_from_condition(self, condition: str) -> Optional[tuple]:
|
|
252
|
+
"""Infer parameter name and schema from a single condition."""
|
|
253
|
+
import re
|
|
254
|
+
|
|
255
|
+
# Pattern: "x > 0" or "x >= 0"
|
|
256
|
+
if match := re.match(r'(\w+)\s*>=?\s*(\d+)', condition):
|
|
257
|
+
param_name = match.group(1)
|
|
258
|
+
min_val = int(match.group(2))
|
|
259
|
+
return (param_name, {
|
|
260
|
+
"type": "integer",
|
|
261
|
+
"minimum": min_val if '>=' in condition else min_val + 1,
|
|
262
|
+
"description": f"Must satisfy: {condition}"
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
# Pattern: "x < 100" or "x <= 100"
|
|
266
|
+
if match := re.match(r'(\w+)\s*<=?\s*(\d+)', condition):
|
|
267
|
+
param_name = match.group(1)
|
|
268
|
+
max_val = int(match.group(2))
|
|
269
|
+
return (param_name, {
|
|
270
|
+
"type": "integer",
|
|
271
|
+
"maximum": max_val if '<=' in condition else max_val - 1,
|
|
272
|
+
"description": f"Must satisfy: {condition}"
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
# Pattern: "x in [1, 2, 3]"
|
|
276
|
+
if match := re.match(r'(\w+)\s+in\s+\[(.+?)\]', condition):
|
|
277
|
+
param_name = match.group(1)
|
|
278
|
+
values = [v.strip().strip('"\"') for v in match.group(2).split(',')]
|
|
279
|
+
# Try to infer if values are integers
|
|
280
|
+
try:
|
|
281
|
+
enum_values = [int(v) for v in values]
|
|
282
|
+
param_type = "integer"
|
|
283
|
+
except ValueError:
|
|
284
|
+
enum_values = values
|
|
285
|
+
param_type = "string"
|
|
286
|
+
|
|
287
|
+
return (param_name, {
|
|
288
|
+
"type": param_type,
|
|
289
|
+
"enum": enum_values,
|
|
290
|
+
"description": f"Must satisfy: {condition}"
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
# Pattern: "x is string" or "x must be string"
|
|
294
|
+
if match := re.search(r'(\w+).*(?:is|must be).*string', condition, re.IGNORECASE):
|
|
295
|
+
param_name = match.group(1)
|
|
296
|
+
return (param_name, {
|
|
297
|
+
"type": "string",
|
|
298
|
+
"description": f"Must satisfy: {condition}"
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
# Pattern: "x is list" or "x is array"
|
|
302
|
+
if match := re.search(r'(\w+).*(?:is|must be).*(list|array)', condition, re.IGNORECASE):
|
|
303
|
+
param_name = match.group(1)
|
|
304
|
+
return (param_name, {
|
|
305
|
+
"type": "array",
|
|
306
|
+
"items": {"type": "string"},
|
|
307
|
+
"description": f"Must satisfy: {condition}"
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
# Pattern: "x is boolean"
|
|
311
|
+
if match := re.search(r'(\w+).*(?:is|must be).*bool', condition, re.IGNORECASE):
|
|
312
|
+
param_name = match.group(1)
|
|
313
|
+
return (param_name, {
|
|
314
|
+
"type": "boolean",
|
|
315
|
+
"description": f"Must satisfy: {condition}"
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
# Pattern: "len(x) > 0" or "length(x) > 0"
|
|
319
|
+
if match := re.search(r'len(?:gth)?\((\w+)\)\s*>\s*(\d+)', condition):
|
|
320
|
+
param_name = match.group(1)
|
|
321
|
+
min_length = int(match.group(2)) + 1
|
|
322
|
+
return (param_name, {
|
|
323
|
+
"type": "string",
|
|
324
|
+
"minLength": min_length,
|
|
325
|
+
"description": f"Must satisfy: {condition}"
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
# Mathematical set notation: "x ∈ ℝ" or "x ∈ ℤ"
|
|
329
|
+
if match := re.search(r'(\w+)\s*∈\s*([ℝℤℕ])', condition):
|
|
330
|
+
param_name = match.group(1)
|
|
331
|
+
set_symbol = match.group(2)
|
|
332
|
+
param_type = "number" if set_symbol == 'ℝ' else "integer"
|
|
333
|
+
return (param_name, {
|
|
334
|
+
"type": param_type,
|
|
335
|
+
"description": f"Must satisfy: {condition}"
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
# Default: try to extract parameter name
|
|
339
|
+
if match := re.match(r'(\w+)', condition):
|
|
340
|
+
param_name = match.group(1)
|
|
341
|
+
return (param_name, {
|
|
342
|
+
"type": "string",
|
|
343
|
+
"description": f"Must satisfy: {condition}"
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
def _extract_response_from_postconditions(self, func: FunctionAnnotation) -> Dict[str, Any]:
|
|
349
|
+
"""Extract response schema from postconditions."""
|
|
350
|
+
response = {
|
|
351
|
+
"result": {
|
|
352
|
+
"type": "object",
|
|
353
|
+
"description": "Operation result"
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if func.postconditions:
|
|
358
|
+
response["result"]["x-postconditions"] = func.postconditions
|
|
359
|
+
|
|
360
|
+
return response
|
|
361
|
+
|
|
362
|
+
def _map_error_to_status_code(self, error_type: str) -> int:
|
|
363
|
+
"""Map error type to HTTP status code."""
|
|
364
|
+
error_mapping = {
|
|
365
|
+
"ValueError": 400,
|
|
366
|
+
"ValidationError": 400,
|
|
367
|
+
"InvalidInputError": 400,
|
|
368
|
+
"InsufficientFundsError": 400,
|
|
369
|
+
"AccountInactiveError": 403,
|
|
370
|
+
"UnauthorizedError": 401,
|
|
371
|
+
"ForbiddenError": 403,
|
|
372
|
+
"NotFoundError": 404,
|
|
373
|
+
"ConflictError": 409,
|
|
374
|
+
"InternalError": 500,
|
|
375
|
+
"TimeoutError": 504,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return error_mapping.get(error_type, 400)
|
|
379
|
+
|
|
380
|
+
def _generate_graphql(self, parsed: ParsedAnnotations, output_file: Optional[str] = None) -> str:
|
|
381
|
+
"""Generate GraphQL schema."""
|
|
382
|
+
lines = []
|
|
383
|
+
|
|
384
|
+
# Schema header
|
|
385
|
+
lines.append('"""')
|
|
386
|
+
lines.append(f"{parsed.module.name.replace('_', ' ').title()} API")
|
|
387
|
+
lines.append("Generated from DarkArts annotations")
|
|
388
|
+
lines.append('"""')
|
|
389
|
+
lines.append("")
|
|
390
|
+
|
|
391
|
+
# Generate types from classes
|
|
392
|
+
for cls in parsed.module.classes:
|
|
393
|
+
lines.append(f"type {cls.name} {{")
|
|
394
|
+
lines.append(f' """')
|
|
395
|
+
if cls.class_invariants:
|
|
396
|
+
lines.append(" Invariants:")
|
|
397
|
+
for inv in cls.class_invariants:
|
|
398
|
+
lines.append(f" - {inv}")
|
|
399
|
+
lines.append(' """')
|
|
400
|
+
lines.append(" id: ID!")
|
|
401
|
+
lines.append(" # TODO: Add fields based on class definition")
|
|
402
|
+
lines.append("}")
|
|
403
|
+
lines.append("")
|
|
404
|
+
|
|
405
|
+
# Generate queries and mutations from functions
|
|
406
|
+
lines.append("type Query {")
|
|
407
|
+
all_functions = parsed.get_all_functions()
|
|
408
|
+
for func in all_functions:
|
|
409
|
+
if func.name.startswith("get_") or func.name.startswith("list_"):
|
|
410
|
+
lines.append(f' {func.name}: String # {func.solve or ""}')
|
|
411
|
+
lines.append("}")
|
|
412
|
+
lines.append("")
|
|
413
|
+
|
|
414
|
+
lines.append("type Mutation {")
|
|
415
|
+
for func in all_functions:
|
|
416
|
+
if not (func.name.startswith("get_") or func.name.startswith("list_")):
|
|
417
|
+
lines.append(f' {func.name}: String # {func.solve or ""}')
|
|
418
|
+
lines.append("}")
|
|
419
|
+
lines.append("")
|
|
420
|
+
|
|
421
|
+
schema = "\n".join(lines)
|
|
422
|
+
|
|
423
|
+
if output_file:
|
|
424
|
+
output_path = Path(output_file)
|
|
425
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
426
|
+
output_path.write_text(schema, encoding='utf-8')
|
|
427
|
+
|
|
428
|
+
return schema
|
|
429
|
+
|
|
430
|
+
def _generate_request_example(self, func: FunctionAnnotation) -> Dict[str, Any]:
|
|
431
|
+
"""Generate example request from preconditions."""
|
|
432
|
+
import re
|
|
433
|
+
example = {}
|
|
434
|
+
|
|
435
|
+
if func.preconditions:
|
|
436
|
+
for pre in func.preconditions:
|
|
437
|
+
# Pattern: "x > 0" → x = 1
|
|
438
|
+
if match := re.match(r'(\w+)\s*>\s*(\d+)', pre):
|
|
439
|
+
param_name = match.group(1)
|
|
440
|
+
min_val = int(match.group(2))
|
|
441
|
+
example[param_name] = min_val + 1
|
|
442
|
+
|
|
443
|
+
# Pattern: "x >= 0" → x = 0
|
|
444
|
+
elif match := re.match(r'(\w+)\s*>=\s*(\d+)', pre):
|
|
445
|
+
param_name = match.group(1)
|
|
446
|
+
min_val = int(match.group(2))
|
|
447
|
+
example[param_name] = min_val
|
|
448
|
+
|
|
449
|
+
# Pattern: "x in [1, 2, 3]" → x = 1
|
|
450
|
+
elif match := re.match(r'(\w+)\s+in\s+\[(.+?)\]', pre):
|
|
451
|
+
param_name = match.group(1)
|
|
452
|
+
values = [v.strip().strip('"\'') for v in match.group(2).split(',')]
|
|
453
|
+
try:
|
|
454
|
+
example[param_name] = int(values[0])
|
|
455
|
+
except ValueError:
|
|
456
|
+
example[param_name] = values[0]
|
|
457
|
+
|
|
458
|
+
# Pattern: "x is string" → x = "example"
|
|
459
|
+
elif match := re.search(r'(\w+).*(?:is|must be).*string', pre, re.IGNORECASE):
|
|
460
|
+
param_name = match.group(1)
|
|
461
|
+
example[param_name] = "example"
|
|
462
|
+
|
|
463
|
+
# Pattern: "x is list" → x = []
|
|
464
|
+
elif match := re.search(r'(\w+).*(?:is|must be).*(list|array)', pre, re.IGNORECASE):
|
|
465
|
+
param_name = match.group(1)
|
|
466
|
+
example[param_name] = ["item1", "item2"]
|
|
467
|
+
|
|
468
|
+
# Pattern: "x is boolean" → x = true
|
|
469
|
+
elif match := re.search(r'(\w+).*(?:is|must be).*bool', pre, re.IGNORECASE):
|
|
470
|
+
param_name = match.group(1)
|
|
471
|
+
example[param_name] = True
|
|
472
|
+
|
|
473
|
+
return example if example else {"input": "example"}
|
|
474
|
+
|
|
475
|
+
def _generate_response_example(self, func: FunctionAnnotation) -> Dict[str, Any]:
|
|
476
|
+
"""Generate example response from postconditions."""
|
|
477
|
+
example = {"result": "success"}
|
|
478
|
+
|
|
479
|
+
if func.postconditions:
|
|
480
|
+
# Try to infer result structure from postconditions
|
|
481
|
+
for post in func.postconditions:
|
|
482
|
+
if "result" in post.lower():
|
|
483
|
+
example["result"] = "computed_value"
|
|
484
|
+
break
|
|
485
|
+
|
|
486
|
+
return example
|