mcp-openapi-generator 1.0.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.
- mcp_openapi_generator/__init__.py +13 -0
- mcp_openapi_generator/cli.py +198 -0
- mcp_openapi_generator/server.py +300 -0
- mcp_openapi_generator/tools/__init__.py +14 -0
- mcp_openapi_generator/tools/generate.py +291 -0
- mcp_openapi_generator/tools/validate.py +277 -0
- mcp_openapi_generator/types/__init__.py +195 -0
- mcp_openapi_generator/utils/__init__.py +21 -0
- mcp_openapi_generator/utils/config_inference.py +219 -0
- mcp_openapi_generator/utils/post_process.py +229 -0
- mcp_openapi_generator/utils/spec_resolver.py +184 -0
- mcp_openapi_generator-1.0.0.dist-info/METADATA +332 -0
- mcp_openapi_generator-1.0.0.dist-info/RECORD +16 -0
- mcp_openapi_generator-1.0.0.dist-info/WHEEL +4 -0
- mcp_openapi_generator-1.0.0.dist-info/entry_points.txt +3 -0
- mcp_openapi_generator-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""Code generation tool using openapi-generator and swagger-codegen."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TypedDict
|
|
12
|
+
|
|
13
|
+
from ..types import GenerationOptions, GenerationResult, GeneratorTarget, SpecInput
|
|
14
|
+
from ..utils.config_inference import infer_project_config
|
|
15
|
+
from ..utils.post_process import post_process_generation
|
|
16
|
+
from ..utils.spec_resolver import resolve_spec
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
__all__ = ["generate_client", "list_generators", "GeneratorWrapper"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GeneratorListResult(TypedDict):
|
|
24
|
+
generator: str
|
|
25
|
+
available: bool
|
|
26
|
+
targets: list[str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Generator executable names
|
|
30
|
+
OPENAPI_GENERATOR_CLI = "openapi-generator-cli"
|
|
31
|
+
SWAGGER_CODEGEN_CLI = "swagger-codegen"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GeneratorWrapper:
|
|
35
|
+
"""Wrapper for OpenAPI code generators."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, prefer: str = "openapi-generator"):
|
|
38
|
+
self.prefer = prefer
|
|
39
|
+
self._openapi_generator_path = shutil.which(OPENAPI_GENERATOR_CLI)
|
|
40
|
+
self._swagger_codegen_path = shutil.which(SWAGGER_CODEGEN_CLI)
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def available(self) -> bool:
|
|
44
|
+
return bool(self._openapi_generator_path or self._swagger_codegen_path)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def generator_name(self) -> str:
|
|
48
|
+
if self._openapi_generator_path and self.prefer == "openapi-generator":
|
|
49
|
+
return "openapi-generator"
|
|
50
|
+
if self._swagger_codegen_path:
|
|
51
|
+
return "swagger-codegen"
|
|
52
|
+
if self._openapi_generator_path:
|
|
53
|
+
return "openapi-generator"
|
|
54
|
+
return "none"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def executable(self) -> str | None:
|
|
58
|
+
if self.generator_name == "openapi-generator":
|
|
59
|
+
return self._openapi_generator_path
|
|
60
|
+
if self.generator_name == "swagger-codegen":
|
|
61
|
+
return self._swagger_codegen_path
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
def get_supported_targets(self) -> list[str]:
|
|
65
|
+
"""Get list of supported generation targets."""
|
|
66
|
+
if not self.executable:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
command = [self.executable, "list"]
|
|
71
|
+
if self.generator_name == "openapi-generator":
|
|
72
|
+
command.append("-s")
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
command,
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
timeout=30,
|
|
78
|
+
)
|
|
79
|
+
if result.returncode == 0:
|
|
80
|
+
if self.generator_name == "openapi-generator":
|
|
81
|
+
return sorted({
|
|
82
|
+
target.strip()
|
|
83
|
+
for target in result.stdout.replace("\n", ",").split(",")
|
|
84
|
+
if target.strip()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
line.split()[0]
|
|
89
|
+
for line in result.stdout.splitlines()
|
|
90
|
+
if line.strip() and not line.strip().startswith("Available")
|
|
91
|
+
]
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.warning("Failed to list generators: %s", e)
|
|
94
|
+
return []
|
|
95
|
+
|
|
96
|
+
async def generate(
|
|
97
|
+
self,
|
|
98
|
+
options: GenerationOptions,
|
|
99
|
+
project_root: Path | None = None,
|
|
100
|
+
) -> GenerationResult:
|
|
101
|
+
"""Generate client code using the configured generator."""
|
|
102
|
+
if not self.executable:
|
|
103
|
+
return GenerationResult(
|
|
104
|
+
success=False,
|
|
105
|
+
output_dir=options.output_dir,
|
|
106
|
+
files_created=[],
|
|
107
|
+
target=options.target,
|
|
108
|
+
spec_source=options.spec.location,
|
|
109
|
+
error=(
|
|
110
|
+
"No OpenAPI generator found. Install openapi-generator-cli or swagger-codegen."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Resolve spec
|
|
115
|
+
try:
|
|
116
|
+
spec = await resolve_spec(options.spec)
|
|
117
|
+
logger.debug("Resolved spec with %d top-level keys", len(spec))
|
|
118
|
+
except Exception as e:
|
|
119
|
+
return GenerationResult(
|
|
120
|
+
success=False,
|
|
121
|
+
output_dir=options.output_dir,
|
|
122
|
+
files_created=[],
|
|
123
|
+
target=options.target,
|
|
124
|
+
spec_source=options.spec.location,
|
|
125
|
+
error=f"Failed to resolve spec: {e}",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Write spec to temp file for generator
|
|
129
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
|
130
|
+
import yaml
|
|
131
|
+
|
|
132
|
+
yaml.dump(spec, f)
|
|
133
|
+
spec_file = Path(f.name)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
return await self._run_generation(options, spec_file, project_root)
|
|
137
|
+
finally:
|
|
138
|
+
spec_file.unlink(missing_ok=True)
|
|
139
|
+
|
|
140
|
+
async def _run_generation(
|
|
141
|
+
self,
|
|
142
|
+
options: GenerationOptions,
|
|
143
|
+
spec_file: Path,
|
|
144
|
+
project_root: Path | None,
|
|
145
|
+
) -> GenerationResult:
|
|
146
|
+
"""Run the generator CLI."""
|
|
147
|
+
executable = self.executable
|
|
148
|
+
if executable is None:
|
|
149
|
+
return GenerationResult(
|
|
150
|
+
success=False,
|
|
151
|
+
output_dir=options.output_dir,
|
|
152
|
+
files_created=[],
|
|
153
|
+
target=options.target,
|
|
154
|
+
spec_source=options.spec.location,
|
|
155
|
+
error="No OpenAPI generator is available.",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
output_dir = options.output_dir
|
|
159
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
160
|
+
|
|
161
|
+
cmd = [
|
|
162
|
+
executable,
|
|
163
|
+
"generate",
|
|
164
|
+
"-i",
|
|
165
|
+
str(spec_file),
|
|
166
|
+
"-g",
|
|
167
|
+
options.target.value,
|
|
168
|
+
"-o",
|
|
169
|
+
str(output_dir),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
# Add optional parameters
|
|
173
|
+
if options.package_name:
|
|
174
|
+
cmd.extend(["--package-name", options.package_name])
|
|
175
|
+
if options.package_version:
|
|
176
|
+
cmd.extend(["--package-version", options.package_version])
|
|
177
|
+
|
|
178
|
+
# Add additional properties
|
|
179
|
+
for key, value in options.additional_properties.items():
|
|
180
|
+
cmd.extend(["--additional-properties", f"{key}={value}"])
|
|
181
|
+
|
|
182
|
+
# Generator-specific options
|
|
183
|
+
if self.generator_name == "openapi-generator":
|
|
184
|
+
if not options.generate_tests:
|
|
185
|
+
cmd.extend(["--global-property", "apiTests=false,modelTests=false"])
|
|
186
|
+
elif self.generator_name == "swagger-codegen":
|
|
187
|
+
# swagger-codegen uses different flags
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
logger.info("Running generation: %s", " ".join(cmd))
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
process = await asyncio.create_subprocess_exec(
|
|
194
|
+
*cmd,
|
|
195
|
+
stdout=asyncio.subprocess.PIPE,
|
|
196
|
+
stderr=asyncio.subprocess.PIPE,
|
|
197
|
+
cwd=project_root,
|
|
198
|
+
)
|
|
199
|
+
stdout, stderr = await process.communicate()
|
|
200
|
+
|
|
201
|
+
if process.returncode != 0:
|
|
202
|
+
error_msg = stderr.decode() if stderr else "Unknown error"
|
|
203
|
+
logger.error("Generation failed: %s", error_msg)
|
|
204
|
+
return GenerationResult(
|
|
205
|
+
success=False,
|
|
206
|
+
output_dir=output_dir,
|
|
207
|
+
files_created=[],
|
|
208
|
+
target=options.target,
|
|
209
|
+
spec_source=options.spec.location,
|
|
210
|
+
error=error_msg[:500],
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Collect generated files
|
|
214
|
+
files_created = list(output_dir.rglob("*"))
|
|
215
|
+
files_created = [f for f in files_created if f.is_file()]
|
|
216
|
+
|
|
217
|
+
return GenerationResult(
|
|
218
|
+
success=True,
|
|
219
|
+
output_dir=output_dir,
|
|
220
|
+
files_created=files_created,
|
|
221
|
+
target=options.target,
|
|
222
|
+
spec_source=options.spec.location,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.exception("Generation error")
|
|
227
|
+
return GenerationResult(
|
|
228
|
+
success=False,
|
|
229
|
+
output_dir=output_dir,
|
|
230
|
+
files_created=[],
|
|
231
|
+
target=options.target,
|
|
232
|
+
spec_source=options.spec.location,
|
|
233
|
+
error=str(e),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def generate_client(
|
|
238
|
+
spec: SpecInput,
|
|
239
|
+
target: GeneratorTarget,
|
|
240
|
+
output_dir: Path,
|
|
241
|
+
package_name: str | None = None,
|
|
242
|
+
package_version: str | None = None,
|
|
243
|
+
additional_properties: dict[str, str] | None = None,
|
|
244
|
+
generate_tests: bool = True,
|
|
245
|
+
generate_docs: bool = False,
|
|
246
|
+
post_process: bool = True,
|
|
247
|
+
project_root: Path | None = None,
|
|
248
|
+
) -> GenerationResult:
|
|
249
|
+
"""
|
|
250
|
+
High-level function to generate a client from an OpenAPI spec.
|
|
251
|
+
|
|
252
|
+
This is the main entry point for the MCP tool.
|
|
253
|
+
"""
|
|
254
|
+
# Infer project config for defaults
|
|
255
|
+
if project_root is None:
|
|
256
|
+
project_root = Path.cwd()
|
|
257
|
+
config = infer_project_config(project_root)
|
|
258
|
+
|
|
259
|
+
# Use inferred target if not specified
|
|
260
|
+
if target is None:
|
|
261
|
+
target = GeneratorTarget.from_project_config(config)
|
|
262
|
+
|
|
263
|
+
options = GenerationOptions(
|
|
264
|
+
target=target,
|
|
265
|
+
spec=spec,
|
|
266
|
+
output_dir=output_dir,
|
|
267
|
+
package_name=package_name,
|
|
268
|
+
package_version=package_version,
|
|
269
|
+
additional_properties=additional_properties or {},
|
|
270
|
+
generate_tests=generate_tests,
|
|
271
|
+
generate_docs=generate_docs,
|
|
272
|
+
post_process=post_process,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
wrapper = GeneratorWrapper()
|
|
276
|
+
result = await wrapper.generate(options, project_root)
|
|
277
|
+
|
|
278
|
+
if result.success and post_process:
|
|
279
|
+
result = await post_process_generation(result, options, config)
|
|
280
|
+
|
|
281
|
+
return result
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def list_generators() -> GeneratorListResult:
|
|
285
|
+
"""List available generators and their targets."""
|
|
286
|
+
wrapper = GeneratorWrapper()
|
|
287
|
+
return {
|
|
288
|
+
"generator": wrapper.generator_name,
|
|
289
|
+
"available": wrapper.available,
|
|
290
|
+
"targets": wrapper.get_supported_targets(),
|
|
291
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Specification validation tool using Spectral and Vacuum."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from ..types import SpecInput, ValidationOptions, ValidationResult, ValidationTool
|
|
12
|
+
from ..utils.spec_resolver import resolve_spec
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
__all__ = ["validate_spec", "ValidationWrapper"]
|
|
17
|
+
|
|
18
|
+
# Validator executable names
|
|
19
|
+
SPECTRAL_CLI = "spectral"
|
|
20
|
+
VACUUM_CLI = "vacuum"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ValidationWrapper:
|
|
24
|
+
"""Wrapper for OpenAPI validation tools."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
self._spectral_path = shutil.which(SPECTRAL_CLI)
|
|
28
|
+
self._vacuum_path = shutil.which(VACUUM_CLI)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def available_tools(self) -> list[ValidationTool]:
|
|
32
|
+
tools = []
|
|
33
|
+
if self._spectral_path:
|
|
34
|
+
tools.append(ValidationTool.SPECTRAL)
|
|
35
|
+
if self._vacuum_path:
|
|
36
|
+
tools.append(ValidationTool.VACUUM)
|
|
37
|
+
return tools
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def preferred_tool(self) -> ValidationTool:
|
|
41
|
+
if ValidationTool.SPECTRAL in self.available_tools:
|
|
42
|
+
return ValidationTool.SPECTRAL
|
|
43
|
+
if ValidationTool.VACUUM in self.available_tools:
|
|
44
|
+
return ValidationTool.VACUUM
|
|
45
|
+
return ValidationTool.SPECTRAL # default
|
|
46
|
+
|
|
47
|
+
def get_executable(self, tool: ValidationTool) -> str | None:
|
|
48
|
+
if tool == ValidationTool.SPECTRAL:
|
|
49
|
+
return self._spectral_path
|
|
50
|
+
if tool == ValidationTool.VACUUM:
|
|
51
|
+
return self._vacuum_path
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
async def validate(
|
|
55
|
+
self,
|
|
56
|
+
options: ValidationOptions,
|
|
57
|
+
) -> ValidationResult:
|
|
58
|
+
"""Validate an OpenAPI specification."""
|
|
59
|
+
tool = options.tool or self.preferred_tool
|
|
60
|
+
executable = self.get_executable(tool)
|
|
61
|
+
|
|
62
|
+
if not executable:
|
|
63
|
+
return ValidationResult(
|
|
64
|
+
success=False,
|
|
65
|
+
valid=False,
|
|
66
|
+
tool=tool,
|
|
67
|
+
spec_source=options.spec.location,
|
|
68
|
+
error=f"Validator {tool.value} not found. Install spectral or vacuum.",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Resolve spec
|
|
72
|
+
try:
|
|
73
|
+
spec = await resolve_spec(options.spec)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return ValidationResult(
|
|
76
|
+
success=False,
|
|
77
|
+
valid=False,
|
|
78
|
+
tool=tool,
|
|
79
|
+
spec_source=options.spec.location,
|
|
80
|
+
error=f"Failed to resolve spec: {e}",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Write spec to temp file
|
|
84
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
|
85
|
+
import yaml
|
|
86
|
+
|
|
87
|
+
yaml.dump(spec, f)
|
|
88
|
+
spec_file = Path(f.name)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
return await self._run_validation(executable, tool, spec_file, options)
|
|
92
|
+
finally:
|
|
93
|
+
spec_file.unlink(missing_ok=True)
|
|
94
|
+
|
|
95
|
+
async def _run_validation(
|
|
96
|
+
self,
|
|
97
|
+
executable: str,
|
|
98
|
+
tool: ValidationTool,
|
|
99
|
+
spec_file: Path,
|
|
100
|
+
options: ValidationOptions,
|
|
101
|
+
) -> ValidationResult:
|
|
102
|
+
"""Run the validation CLI."""
|
|
103
|
+
if tool == ValidationTool.SPECTRAL:
|
|
104
|
+
cmd = [executable, "lint", str(spec_file), "--format", "json"]
|
|
105
|
+
if options.ruleset:
|
|
106
|
+
cmd.extend(["--ruleset", options.ruleset])
|
|
107
|
+
elif tool == ValidationTool.VACUUM:
|
|
108
|
+
cmd = [executable, "lint", str(spec_file), "-format", "json"]
|
|
109
|
+
if options.ruleset:
|
|
110
|
+
cmd.extend(["-ruleset", options.ruleset])
|
|
111
|
+
else:
|
|
112
|
+
return ValidationResult(
|
|
113
|
+
success=False,
|
|
114
|
+
valid=False,
|
|
115
|
+
tool=tool,
|
|
116
|
+
spec_source=options.spec.location,
|
|
117
|
+
error=f"Unknown validation tool: {tool}",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
logger.info("Running validation: %s", " ".join(cmd))
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
process = await asyncio.create_subprocess_exec(
|
|
124
|
+
*cmd,
|
|
125
|
+
stdout=asyncio.subprocess.PIPE,
|
|
126
|
+
stderr=asyncio.subprocess.PIPE,
|
|
127
|
+
)
|
|
128
|
+
stdout, stderr = await process.communicate()
|
|
129
|
+
|
|
130
|
+
# Parse results
|
|
131
|
+
if tool == ValidationTool.SPECTRAL:
|
|
132
|
+
return self._parse_spectral_output(
|
|
133
|
+
stdout.decode() if stdout else "[]",
|
|
134
|
+
options.spec.location,
|
|
135
|
+
options.fail_on_warnings,
|
|
136
|
+
)
|
|
137
|
+
if tool == ValidationTool.VACUUM:
|
|
138
|
+
return self._parse_vacuum_output(
|
|
139
|
+
stdout.decode() if stdout else "[]",
|
|
140
|
+
options.spec.location,
|
|
141
|
+
options.fail_on_warnings,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
except Exception as e:
|
|
145
|
+
logger.exception("Validation error")
|
|
146
|
+
return ValidationResult(
|
|
147
|
+
success=False,
|
|
148
|
+
valid=False,
|
|
149
|
+
tool=tool,
|
|
150
|
+
spec_source=options.spec.location,
|
|
151
|
+
error=str(e),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _parse_spectral_output(
|
|
155
|
+
self,
|
|
156
|
+
stdout: str,
|
|
157
|
+
spec_source: str,
|
|
158
|
+
fail_on_warnings: bool = False,
|
|
159
|
+
) -> ValidationResult:
|
|
160
|
+
"""Parse Spectral JSON output."""
|
|
161
|
+
import json
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
results = json.loads(stdout) if stdout.strip() else []
|
|
165
|
+
except json.JSONDecodeError:
|
|
166
|
+
return ValidationResult(
|
|
167
|
+
success=False,
|
|
168
|
+
valid=False,
|
|
169
|
+
tool=ValidationTool.SPECTRAL,
|
|
170
|
+
spec_source=spec_source,
|
|
171
|
+
error=f"Failed to parse Spectral output: {stdout[:200]}",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
errors = []
|
|
175
|
+
warnings = []
|
|
176
|
+
|
|
177
|
+
for result in results:
|
|
178
|
+
severity_value = result.get("severity", "error")
|
|
179
|
+
if isinstance(severity_value, int):
|
|
180
|
+
severity = {0: "error", 1: "warning", 2: "info", 3: "hint"}.get(
|
|
181
|
+
severity_value, "error"
|
|
182
|
+
)
|
|
183
|
+
else:
|
|
184
|
+
severity = str(severity_value).lower()
|
|
185
|
+
message = {
|
|
186
|
+
"code": result.get("code", ""),
|
|
187
|
+
"message": result.get("message", ""),
|
|
188
|
+
"path": result.get("path", ""),
|
|
189
|
+
"range": result.get("range", {}),
|
|
190
|
+
"severity": severity,
|
|
191
|
+
}
|
|
192
|
+
if severity == "error":
|
|
193
|
+
errors.append(message)
|
|
194
|
+
else:
|
|
195
|
+
warnings.append(message)
|
|
196
|
+
|
|
197
|
+
valid = len(errors) == 0
|
|
198
|
+
if fail_on_warnings:
|
|
199
|
+
valid = valid and len(warnings) == 0
|
|
200
|
+
|
|
201
|
+
return ValidationResult(
|
|
202
|
+
success=True,
|
|
203
|
+
valid=valid,
|
|
204
|
+
tool=ValidationTool.SPECTRAL,
|
|
205
|
+
spec_source=spec_source,
|
|
206
|
+
errors=errors,
|
|
207
|
+
warnings=warnings,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _parse_vacuum_output(
|
|
211
|
+
self,
|
|
212
|
+
stdout: str,
|
|
213
|
+
spec_source: str,
|
|
214
|
+
fail_on_warnings: bool = False,
|
|
215
|
+
) -> ValidationResult:
|
|
216
|
+
"""Parse Vacuum JSON output."""
|
|
217
|
+
import json
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
results = json.loads(stdout) if stdout.strip() else []
|
|
221
|
+
except json.JSONDecodeError:
|
|
222
|
+
return ValidationResult(
|
|
223
|
+
success=False,
|
|
224
|
+
valid=False,
|
|
225
|
+
tool=ValidationTool.VACUUM,
|
|
226
|
+
spec_source=spec_source,
|
|
227
|
+
error=f"Failed to parse Vacuum output: {stdout[:200]}",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
errors = []
|
|
231
|
+
warnings = []
|
|
232
|
+
|
|
233
|
+
for result in results:
|
|
234
|
+
# Vacuum output format differs slightly
|
|
235
|
+
severity = result.get("severity", "error").lower()
|
|
236
|
+
message = {
|
|
237
|
+
"code": result.get("code", ""),
|
|
238
|
+
"message": result.get("message", ""),
|
|
239
|
+
"path": result.get("path", ""),
|
|
240
|
+
"line": result.get("line", 0),
|
|
241
|
+
"severity": severity,
|
|
242
|
+
}
|
|
243
|
+
if severity == "error":
|
|
244
|
+
errors.append(message)
|
|
245
|
+
else:
|
|
246
|
+
warnings.append(message)
|
|
247
|
+
|
|
248
|
+
valid = len(errors) == 0
|
|
249
|
+
if fail_on_warnings:
|
|
250
|
+
valid = valid and len(warnings) == 0
|
|
251
|
+
|
|
252
|
+
return ValidationResult(
|
|
253
|
+
success=True,
|
|
254
|
+
valid=valid,
|
|
255
|
+
tool=ValidationTool.VACUUM,
|
|
256
|
+
spec_source=spec_source,
|
|
257
|
+
errors=errors,
|
|
258
|
+
warnings=warnings,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def validate_spec(
|
|
263
|
+
spec: SpecInput,
|
|
264
|
+
tool: ValidationTool = ValidationTool.SPECTRAL,
|
|
265
|
+
ruleset: str | None = None,
|
|
266
|
+
fail_on_warnings: bool = False,
|
|
267
|
+
) -> ValidationResult:
|
|
268
|
+
"""High-level function to validate an OpenAPI specification."""
|
|
269
|
+
options = ValidationOptions(
|
|
270
|
+
spec=spec,
|
|
271
|
+
tool=tool,
|
|
272
|
+
ruleset=ruleset,
|
|
273
|
+
fail_on_warnings=fail_on_warnings,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
wrapper = ValidationWrapper()
|
|
277
|
+
return await wrapper.validate(options)
|