Merge "Parse schema for validation"
This commit is contained in:
commit
bbd3efe650
@ -61,6 +61,27 @@ class MetadataGenerator(BaseGenerator):
|
||||
|
||||
schema = self.load_openapi(spec_path)
|
||||
openapi_spec = common.get_openapi_spec(spec_path)
|
||||
metadata = self.build_metadata(
|
||||
schema, openapi_spec, args.service_type, spec_path.as_posix()
|
||||
)
|
||||
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
yaml.default_flow_style = False
|
||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||
metadata_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
with open(metadata_path, "w") as fp:
|
||||
yaml.dump(
|
||||
metadata.model_dump(
|
||||
exclude_none=True, exclude_defaults=True, by_alias=True
|
||||
),
|
||||
fp,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def build_metadata(
|
||||
schema, openapi_spec, service_type: str, spec_path: str
|
||||
) -> Metadata:
|
||||
metadata = Metadata(resources={})
|
||||
api_ver = "v" + schema.info["version"].split(".")[0]
|
||||
for path, spec in schema.paths.items():
|
||||
@ -68,7 +89,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
resource_name = "/".join(
|
||||
list(common.get_resource_names_from_url(path))
|
||||
)
|
||||
if args.service_type == "object-store":
|
||||
if service_type == "object-store":
|
||||
if path == "/v1/{account}":
|
||||
resource_name = "account"
|
||||
elif path == "/v1/{account}/{container}":
|
||||
@ -76,7 +97,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
if path == "/v1/{account}/{object}":
|
||||
resource_name = "object"
|
||||
|
||||
if args.service_type == "compute" and resource_name in [
|
||||
if service_type == "compute" and resource_name in [
|
||||
"agent",
|
||||
"baremetal_node",
|
||||
"cell",
|
||||
@ -111,11 +132,9 @@ class MetadataGenerator(BaseGenerator):
|
||||
# We do not need to produce anything for deprecated APIs
|
||||
continue
|
||||
resource_model = metadata.resources.setdefault(
|
||||
f"{args.service_type}.{resource_name}",
|
||||
f"{service_type}.{resource_name}",
|
||||
ResourceModel(
|
||||
api_version=api_ver,
|
||||
spec_file=spec_path.as_posix(),
|
||||
operations={},
|
||||
api_version=api_ver, spec_file=spec_path, operations={}
|
||||
),
|
||||
)
|
||||
for method in [
|
||||
@ -187,44 +206,44 @@ class MetadataGenerator(BaseGenerator):
|
||||
if method == "post":
|
||||
operation_key = "update"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
service_type == "compute"
|
||||
and resource_name == "flavor/flavor_access"
|
||||
and method == "get"
|
||||
):
|
||||
operation_key = "list"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
service_type == "compute"
|
||||
and resource_name == "aggregate/image"
|
||||
and method == "post"
|
||||
):
|
||||
operation_key = "action"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
service_type == "compute"
|
||||
and resource_name == "server/security_group"
|
||||
and method == "get"
|
||||
):
|
||||
operation_key = "list"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
service_type == "compute"
|
||||
and resource_name == "server/topology"
|
||||
and method == "get"
|
||||
):
|
||||
operation_key = "list"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
service_type == "compute"
|
||||
and resource_name == "quota_set"
|
||||
and path.endswith("defaults")
|
||||
):
|
||||
operation_key = "defaults"
|
||||
elif (
|
||||
args.service_type == "compute"
|
||||
service_type == "compute"
|
||||
and resource_name == "quota_set"
|
||||
and path.endswith("detail")
|
||||
):
|
||||
# normalize "details" name
|
||||
operation_key = "details"
|
||||
elif (
|
||||
args.service_type == "load-balancer"
|
||||
service_type == "load-balancer"
|
||||
and len(path_elements) > 1
|
||||
and path_elements[-1]
|
||||
in ["stats", "status", "failover", "config"]
|
||||
@ -249,22 +268,22 @@ class MetadataGenerator(BaseGenerator):
|
||||
elif path.endswith("/action"):
|
||||
# Action
|
||||
operation_key = "action"
|
||||
elif args.service_type == "image" and path.endswith(
|
||||
elif service_type == "image" and path.endswith(
|
||||
"/actions/deactivate"
|
||||
):
|
||||
operation_key = "deactivate"
|
||||
elif args.service_type == "image" and path.endswith(
|
||||
elif service_type == "image" and path.endswith(
|
||||
"/actions/reactivate"
|
||||
):
|
||||
operation_key = "reactivate"
|
||||
elif (
|
||||
args.service_type == "block-storage"
|
||||
service_type == "block-storage"
|
||||
and "volume-transfer" in path
|
||||
and path.endswith("/accept")
|
||||
):
|
||||
operation_key = "accept"
|
||||
elif (
|
||||
args.service_type == "block-storage"
|
||||
service_type == "block-storage"
|
||||
and "qos-specs" in path
|
||||
and path_elements[-1]
|
||||
in [
|
||||
@ -276,26 +295,26 @@ class MetadataGenerator(BaseGenerator):
|
||||
):
|
||||
operation_key = path_elements[-1]
|
||||
elif (
|
||||
args.service_type == "network"
|
||||
service_type == "network"
|
||||
and "quota" in path
|
||||
and path.endswith("/default")
|
||||
):
|
||||
# normalize "defaults" name
|
||||
operation_key = "defaults"
|
||||
elif (
|
||||
args.service_type == "network"
|
||||
service_type == "network"
|
||||
and "quota" in path
|
||||
and path.endswith("/details")
|
||||
):
|
||||
operation_key = "details"
|
||||
elif (
|
||||
args.service_type == "placement"
|
||||
service_type == "placement"
|
||||
and resource_name == "allocation_candidate"
|
||||
and method == "get"
|
||||
):
|
||||
operation_key = "list"
|
||||
elif (
|
||||
args.service_type == "placement"
|
||||
service_type == "placement"
|
||||
and resource_name
|
||||
in [
|
||||
"resource_provider/aggregate",
|
||||
@ -357,7 +376,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
)
|
||||
|
||||
# Next hacks
|
||||
if args.service_type == "identity" and resource_name in [
|
||||
if service_type == "identity" and resource_name in [
|
||||
"OS_FEDERATION/identity_provider",
|
||||
"OS_FEDERATION/identity_provider/protocol",
|
||||
"OS_FEDERATION/mapping",
|
||||
@ -368,7 +387,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
elif method == "patch":
|
||||
operation_key = "update"
|
||||
if (
|
||||
args.service_type == "identity"
|
||||
service_type == "identity"
|
||||
and resource_name
|
||||
in [
|
||||
"domain/config",
|
||||
@ -381,7 +400,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
operation_key = "default"
|
||||
|
||||
if (
|
||||
args.service_type == "identity"
|
||||
service_type == "identity"
|
||||
and resource_name
|
||||
in [
|
||||
"domain/config",
|
||||
@ -393,7 +412,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
):
|
||||
# No need in HEAD defaults
|
||||
continue
|
||||
if args.service_type == "object-store":
|
||||
if service_type == "object-store":
|
||||
if resource_name == "object":
|
||||
mapping_obj: dict[str, str] = {
|
||||
"head": "head",
|
||||
@ -421,7 +440,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
"post": "update",
|
||||
}
|
||||
operation_key = mapping_account[method]
|
||||
if args.service_type == "dns":
|
||||
if service_type == "dns":
|
||||
if resource_name == "zone/task":
|
||||
if path == "/v2/zones/{zone_id}/tasks/xfr":
|
||||
operation_key = "xfr"
|
||||
@ -438,9 +457,10 @@ class MetadataGenerator(BaseGenerator):
|
||||
if operation_key in resource_model:
|
||||
raise RuntimeError("Operation name conflict")
|
||||
else:
|
||||
if operation_key == "action" and args.service_type in [
|
||||
if operation_key == "action" and service_type in [
|
||||
"compute",
|
||||
"block-storage",
|
||||
"shared-file-system",
|
||||
]:
|
||||
# For action we actually have multiple independent operations
|
||||
try:
|
||||
@ -522,7 +542,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
)
|
||||
|
||||
op_model = post_process_operation(
|
||||
args.service_type,
|
||||
service_type,
|
||||
resource_name,
|
||||
operation_name,
|
||||
op_model,
|
||||
@ -553,13 +573,13 @@ class MetadataGenerator(BaseGenerator):
|
||||
|
||||
op_model.targets["rust-sdk"] = rust_sdk_params
|
||||
if rust_cli_params and not (
|
||||
args.service_type == "identity"
|
||||
service_type == "identity"
|
||||
and operation_key == "check"
|
||||
):
|
||||
op_model.targets["rust-cli"] = rust_cli_params
|
||||
|
||||
op_model = post_process_operation(
|
||||
args.service_type,
|
||||
service_type,
|
||||
resource_name,
|
||||
operation_key,
|
||||
op_model,
|
||||
@ -587,7 +607,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
openapi_spec, show_op.operation_id
|
||||
)
|
||||
mod_path = common.get_rust_sdk_mod_path(
|
||||
args.service_type, res_data.api_version or "", path
|
||||
service_type, res_data.api_version or "", path
|
||||
)
|
||||
response_schema = None
|
||||
for code, rspec in spec.get("responses", {}).items():
|
||||
@ -662,18 +682,7 @@ class MetadataGenerator(BaseGenerator):
|
||||
if target_name in ["rust-cli"]:
|
||||
target_params.find_implemented_by_sdk = True
|
||||
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
yaml.default_flow_style = False
|
||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||
metadata_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
with open(metadata_path, "w") as fp:
|
||||
yaml.dump(
|
||||
metadata.model_dump(
|
||||
exclude_none=True, exclude_defaults=True, by_alias=True
|
||||
),
|
||||
fp,
|
||||
)
|
||||
return metadata
|
||||
|
||||
|
||||
def get_operation_type_by_key(operation_key):
|
||||
|
@ -16,17 +16,23 @@ import datetime
|
||||
import enum
|
||||
import importlib
|
||||
import inspect
|
||||
import itertools
|
||||
import jsonref
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Literal
|
||||
import re
|
||||
|
||||
from codegenerator import common
|
||||
from codegenerator.common.schema import ParameterSchema
|
||||
from codegenerator.common.schema import PathSchema
|
||||
from codegenerator.common.schema import SpecSchema
|
||||
from codegenerator.common.schema import TypeSchema
|
||||
from codegenerator.metadata import MetadataGenerator
|
||||
from codegenerator import model
|
||||
from codegenerator.openapi.utils import rst_to_md
|
||||
from openapi_core import Spec
|
||||
from openapi_spec_validator import validate
|
||||
from ruamel.yaml.scalarstring import LiteralScalarString
|
||||
from ruamel.yaml import YAML
|
||||
from wsme import types as wtypes
|
||||
@ -116,10 +122,10 @@ class OpenStackServerSourceBase:
|
||||
|
||||
return SpecSchema(**spec)
|
||||
|
||||
def dump_openapi(self, spec, path, validate=False):
|
||||
def dump_openapi(self, spec, path, validate: bool, service_type: str):
|
||||
"""Dump OpenAPI spec into the file"""
|
||||
if validate:
|
||||
self.validate_spec(spec)
|
||||
self.validate_spec(spec, service_type)
|
||||
yaml = YAML()
|
||||
yaml.preserve_quotes = True
|
||||
yaml.indent(mapping=2, sequence=4, offset=2)
|
||||
@ -131,12 +137,90 @@ class OpenStackServerSourceBase:
|
||||
fp,
|
||||
)
|
||||
|
||||
def validate_spec(self, openapi_spec):
|
||||
Spec.from_dict(
|
||||
openapi_spec.model_dump(
|
||||
exclude_none=True, exclude_defaults=True, by_alias=True
|
||||
)
|
||||
def validate_spec(self, openapi_spec, service_type: str):
|
||||
# Perform openapi validation
|
||||
model_data = openapi_spec.model_dump(
|
||||
exclude_none=True, exclude_defaults=True, by_alias=True
|
||||
)
|
||||
Spec.from_dict(model_data)
|
||||
validate(model_data)
|
||||
|
||||
openapi_spec = Spec.from_dict(
|
||||
jsonref.replace_refs(model_data, proxies=False)
|
||||
)
|
||||
|
||||
# Build the metadata as if we would do this normally
|
||||
metadata = MetadataGenerator.build_metadata(
|
||||
SpecSchema(**jsonref.replace_refs(model_data, proxies=False)),
|
||||
openapi_spec,
|
||||
service_type,
|
||||
"",
|
||||
)
|
||||
# Try to parse schema as if we would be doing for generating the code
|
||||
openapi_parser = model.OpenAPISchemaParser()
|
||||
for res, res_spec in metadata.resources.items():
|
||||
for operation_name, operation_spec in res_spec.operations.items():
|
||||
operation_id = operation_spec.operation_id
|
||||
|
||||
(path, method, spec) = common.find_openapi_operation(
|
||||
openapi_spec, operation_id
|
||||
)
|
||||
resource_name = common.get_resource_names_from_url(path)[-1]
|
||||
|
||||
# Parse params
|
||||
for param in openapi_spec["paths"][path].get("parameters", []):
|
||||
openapi_parser.parse_parameter(param)
|
||||
|
||||
op_name: str | None = None
|
||||
response_key: str | None = None
|
||||
sdk_target = operation_spec.targets.get("rust-sdk")
|
||||
if sdk_target:
|
||||
op_name = sdk_target.operation_name
|
||||
response_key = sdk_target.response_key
|
||||
|
||||
operation_variants = common.get_operation_variants(
|
||||
spec, op_name or operation_name
|
||||
)
|
||||
|
||||
for operation_variant in operation_variants:
|
||||
operation_body = operation_variant.get("body")
|
||||
if operation_body:
|
||||
openapi_parser.parse(
|
||||
operation_body, ignore_read_only=True
|
||||
)
|
||||
|
||||
if method.upper() != "HEAD":
|
||||
response = common.find_response_schema(
|
||||
spec["responses"],
|
||||
response_key or resource_name,
|
||||
(
|
||||
operation_name
|
||||
if operation_spec.operation_type == "action"
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
if response:
|
||||
if response_key:
|
||||
response_key = (
|
||||
response_key
|
||||
if response_key != "null"
|
||||
else None
|
||||
)
|
||||
else:
|
||||
response_key = resource_name
|
||||
response_def, _ = common.find_resource_schema(
|
||||
response, None, response_key
|
||||
)
|
||||
|
||||
if response_def:
|
||||
if response_def.get(
|
||||
"type", "object"
|
||||
) == "object" or (
|
||||
isinstance(response_def.get("type"), list)
|
||||
and "object" in response_def["type"]
|
||||
):
|
||||
openapi_parser.parse(response_def)
|
||||
|
||||
def _sanitize_param_ver_info(self, openapi_spec, min_api_version):
|
||||
# Remove min_version of params if it matches to min_api_version
|
||||
|
@ -173,7 +173,9 @@ class CinderV3Generator(OpenStackServerSourceBase):
|
||||
if args.api_ref_src:
|
||||
merge_api_ref_doc(openapi_spec, args.api_ref_src)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
self.dump_openapi(
|
||||
openapi_spec, impl_path, args.validate, "block-storage"
|
||||
)
|
||||
|
||||
lnk = Path(impl_path.parent, "v3.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
|
@ -250,7 +250,7 @@ class DesignateGenerator(OpenStackServerSourceBase):
|
||||
openapi_spec, args.api_ref_src, allow_strip_version=False
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, Path(impl_path), args.validate)
|
||||
self.dump_openapi(openapi_spec, Path(impl_path), args.validate, "dns")
|
||||
|
||||
lnk = Path(impl_path.parent, "v2.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
|
@ -332,7 +332,7 @@ class GlanceGenerator(OpenStackServerSourceBase):
|
||||
if args.api_ref_src:
|
||||
merge_api_ref_doc(openapi_spec, args.api_ref_src)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate, "image")
|
||||
|
||||
lnk = Path(impl_path.parent, "v2.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
|
@ -192,7 +192,9 @@ class IronicGenerator(OpenStackServerSourceBase):
|
||||
openapi_spec, args.api_ref_src, allow_strip_version=False
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, Path(impl_path), args.validate)
|
||||
self.dump_openapi(
|
||||
openapi_spec, Path(impl_path), args.validate, "baremetal"
|
||||
)
|
||||
|
||||
lnk = Path(impl_path.parent, "v1.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
|
@ -160,7 +160,7 @@ class KeystoneGenerator(OpenStackServerSourceBase):
|
||||
openapi_spec, args.api_ref_src, allow_strip_version=False
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate, "image")
|
||||
|
||||
lnk = Path(impl_path.parent, "v3.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
@ -500,7 +500,7 @@ class KeystoneGenerator(OpenStackServerSourceBase):
|
||||
# Ensure operation tags are existing
|
||||
for tag in operation_spec.tags:
|
||||
if tag not in [x["name"] for x in openapi_spec.tags]:
|
||||
openapi_spec.tags.append({"name": tag, "description": None})
|
||||
openapi_spec.tags.append({"name": tag})
|
||||
|
||||
self._post_process_operation_hook(
|
||||
openapi_spec, operation_spec, path=path
|
||||
|
@ -122,7 +122,9 @@ class ManilaGenerator(OpenStackServerSourceBase):
|
||||
openapi_spec, args.api_ref_src, allow_strip_version=False
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
self.dump_openapi(
|
||||
openapi_spec, impl_path, args.validate, "shared-file-system"
|
||||
)
|
||||
|
||||
lnk = Path(impl_path.parent, "v2.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
|
@ -180,7 +180,7 @@ class NeutronGenerator(OpenStackServerSourceBase):
|
||||
# Add base resource routes exposed as a pecan app
|
||||
self._process_base_resource_routes(openapi_spec, processed_routes)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate, "network")
|
||||
|
||||
def process_neutron_with_vpnaas(self, work_dir, processed_routes, args):
|
||||
"""Setup base Neutron with enabled vpnaas"""
|
||||
@ -240,7 +240,7 @@ class NeutronGenerator(OpenStackServerSourceBase):
|
||||
|
||||
(impl_path, openapi_spec) = self._read_spec(work_dir)
|
||||
self._process_router(router, openapi_spec, processed_routes)
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate, "network")
|
||||
|
||||
def _read_spec(self, work_dir):
|
||||
"""Read the spec from file or create an empty one"""
|
||||
@ -373,7 +373,9 @@ class NeutronGenerator(OpenStackServerSourceBase):
|
||||
openapi_spec, args.api_ref_src, allow_strip_version=False
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, Path(impl_path), args.validate)
|
||||
self.dump_openapi(
|
||||
openapi_spec, Path(impl_path), args.validate, "network"
|
||||
)
|
||||
|
||||
return impl_path
|
||||
|
||||
|
@ -103,7 +103,7 @@ class NovaGenerator(OpenStackServerSourceBase):
|
||||
doc_url_prefix="/v2.1",
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate, "compute")
|
||||
|
||||
lnk = Path(impl_path.parent, "v2.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
|
@ -951,7 +951,9 @@ class OctaviaGenerator(OpenStackServerSourceBase):
|
||||
openapi_spec, args.api_ref_src, allow_strip_version=False
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, Path(impl_path), args.validate)
|
||||
self.dump_openapi(
|
||||
openapi_spec, Path(impl_path), args.validate, "load-balancer"
|
||||
)
|
||||
|
||||
lnk = Path(impl_path.parent, "v2.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
|
@ -121,7 +121,7 @@ class PlacementGenerator(OpenStackServerSourceBase):
|
||||
openapi_spec, args.api_ref_src, allow_strip_version=False
|
||||
)
|
||||
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate)
|
||||
self.dump_openapi(openapi_spec, impl_path, args.validate, "placement")
|
||||
|
||||
lnk = Path(impl_path.parent, "v1.yaml")
|
||||
lnk.unlink(missing_ok=True)
|
||||
|
@ -17,6 +17,7 @@
|
||||
--work-dir {{ ansible_user_dir }}/{{ codegenerator_work_dir }}
|
||||
--target openapi-spec
|
||||
--service-type {{ openapi_service }}
|
||||
--validate
|
||||
{%- if codegenerator_api_ref is defined and codegenerator_api_ref is mapping %}
|
||||
--api-ref-src {{ ansible_user_dir }}/{{ zuul.projects[codegenerator_api_ref.project].src_dir }}/{{ codegenerator_api_ref.path | default("/api-ref/build/html/index.html") }}
|
||||
{% endif %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user