|
38 | 38 | ] |
39 | 39 | OPENAPI_SCHEMA = ROOT / "Part2-API-Schemas/openapi.yaml" |
40 | 40 | COMBINED_OPENAPI = ROOT / "Entire-API-Collection/V3.2.yaml" |
| 41 | +MULTIPART_OPENAPI_FILES = ( |
| 42 | + COMBINED_OPENAPI, |
| 43 | + *sorted(ROOT.glob("*ServiceSpecification/V3.2*.yaml")), |
| 44 | +) |
41 | 45 | OPERATION_RIGHT_MAPPING = ROOT_MODULE / "pages/annex/operation-to-right-mapping.adoc" |
42 | 46 | QUERY_EXAMPLES = [ |
43 | 47 | ROOT_MODULE / "pages/http-rest-api/test/query/test1.json", |
@@ -414,6 +418,76 @@ def parse_combined_openapi_operations( |
414 | 418 | return operations |
415 | 419 |
|
416 | 420 |
|
| 421 | +def validate_multipart_file_uploads(validation: Validation) -> None: |
| 422 | + uploads_found = 0 |
| 423 | + |
| 424 | + for openapi_path in MULTIPART_OPENAPI_FILES: |
| 425 | + try: |
| 426 | + document = yaml.safe_load(openapi_path.read_text(encoding="utf-8-sig")) |
| 427 | + except Exception as exc: # noqa: BLE001 - keep parser detail in output. |
| 428 | + validation.fail(openapi_path, f"invalid YAML: {exc}") |
| 429 | + continue |
| 430 | + |
| 431 | + paths = document.get("paths") if isinstance(document, dict) else None |
| 432 | + if not isinstance(paths, dict): |
| 433 | + validation.fail(openapi_path, "missing or invalid paths object") |
| 434 | + continue |
| 435 | + |
| 436 | + for path, path_item in paths.items(): |
| 437 | + if not isinstance(path_item, dict): |
| 438 | + continue |
| 439 | + for method in OPENAPI_HTTP_METHODS: |
| 440 | + operation = path_item.get(method) |
| 441 | + if not isinstance(operation, dict): |
| 442 | + continue |
| 443 | + |
| 444 | + request_body = operation.get("requestBody") |
| 445 | + if not isinstance(request_body, dict): |
| 446 | + continue |
| 447 | + content = request_body.get("content") |
| 448 | + if not isinstance(content, dict): |
| 449 | + continue |
| 450 | + multipart = content.get("multipart/form-data") |
| 451 | + if not isinstance(multipart, dict): |
| 452 | + continue |
| 453 | + schema = multipart.get("schema") |
| 454 | + if not isinstance(schema, dict): |
| 455 | + continue |
| 456 | + properties = schema.get("properties") |
| 457 | + if not isinstance(properties, dict) or "file" not in properties: |
| 458 | + continue |
| 459 | + |
| 460 | + uploads_found += 1 |
| 461 | + location = f"{method.upper()} {path}" |
| 462 | + if "fileName" in properties: |
| 463 | + validation.fail(openapi_path, f"{location} declares a separate fileName multipart part") |
| 464 | + |
| 465 | + required = schema.get("required") |
| 466 | + if not isinstance(required, list) or "file" not in required: |
| 467 | + validation.fail(openapi_path, f"{location} does not require the binary file part") |
| 468 | + |
| 469 | + file_schema = properties["file"] |
| 470 | + if not isinstance(file_schema, dict): |
| 471 | + validation.fail(openapi_path, f"{location} has an invalid file schema") |
| 472 | + continue |
| 473 | + if file_schema.get("type") != "string" or file_schema.get("format") != "binary": |
| 474 | + validation.fail(openapi_path, f"{location} file part is not a binary string") |
| 475 | + |
| 476 | + description = file_schema.get("description") |
| 477 | + if ( |
| 478 | + not isinstance(description, str) |
| 479 | + or "filename" not in description |
| 480 | + or "Content-Disposition" not in description |
| 481 | + ): |
| 482 | + validation.fail( |
| 483 | + openapi_path, |
| 484 | + f"{location} does not document the standard multipart filename parameter", |
| 485 | + ) |
| 486 | + |
| 487 | + if uploads_found == 0: |
| 488 | + validation.fail(ROOT, "no multipart file uploads found") |
| 489 | + |
| 490 | + |
417 | 491 | def validate_operation_right_mapping(validation: Validation) -> None: |
418 | 492 | operations = parse_combined_openapi_operations(validation) |
419 | 493 | rows: dict[str, tuple[str, str, str, str, int]] = {} |
@@ -479,6 +553,7 @@ def main() -> int: |
479 | 553 | validation = Validation() |
480 | 554 | validate_json_schema(validation) |
481 | 555 | validate_bnf_grammar_files(validation) |
| 556 | + validate_multipart_file_uploads(validation) |
482 | 557 | validate_operation_right_mapping(validation) |
483 | 558 | validation.assert_ok() |
484 | 559 | print("OK: spec artifact validation completed") |
|
0 commit comments