mirror of
https://github.com/ceph/ceph
synced 2025-02-23 02:57:21 +00:00
Merge pull request #37375 from ceph/47615-fix-endpoint-responses
Reviewed-by: Ernesto Puerta <epuertat@redhat.com> Reviewed-by: Laura Paduano <lpaduano@suse.com> Reviewed-by: Tatjana Dehler <tdehler@suse.com>
This commit is contained in:
commit
661aa031ee
1
src/pybind/mgr/dashboard/api/__init__.py
Normal file
1
src/pybind/mgr/dashboard/api/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from __future__ import absolute_import
|
53
src/pybind/mgr/dashboard/api/doc.py
Normal file
53
src/pybind/mgr/dashboard/api/doc.py
Normal file
@ -0,0 +1,53 @@
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
class SchemaType(Enum):
|
||||
"""
|
||||
Representation of the type property of a schema object:
|
||||
http://spec.openapis.org/oas/v3.0.3.html#schema-object
|
||||
"""
|
||||
ARRAY = 'array'
|
||||
BOOLEAN = 'boolean'
|
||||
INTEGER = 'integer'
|
||||
NUMBER = 'number'
|
||||
OBJECT = 'object'
|
||||
STRING = 'string'
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
|
||||
class Schema:
|
||||
"""
|
||||
Representation of a schema object:
|
||||
http://spec.openapis.org/oas/v3.0.3.html#schema-object
|
||||
"""
|
||||
|
||||
def __init__(self, schema_type: SchemaType = SchemaType.OBJECT,
|
||||
properties: Optional[Dict] = None, required: Optional[List] = None):
|
||||
self._type = schema_type
|
||||
self._properties = properties if properties else {}
|
||||
self._required = required if required else []
|
||||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
schema: Dict[str, Any] = {'type': str(self._type)}
|
||||
|
||||
if self._type == SchemaType.ARRAY:
|
||||
items = Schema(properties=self._properties)
|
||||
schema['items'] = items.as_dict()
|
||||
else:
|
||||
schema['properties'] = self._properties
|
||||
|
||||
if self._required:
|
||||
schema['required'] = self._required
|
||||
|
||||
return schema
|
||||
|
||||
|
||||
class SchemaInput:
|
||||
"""
|
||||
Simple DTO to transfer data in a structured manner for creating a schema object.
|
||||
"""
|
||||
type: SchemaType
|
||||
params: List[Any]
|
@ -11,12 +11,13 @@ import os
|
||||
import pkgutil
|
||||
import re
|
||||
import sys
|
||||
import urllib
|
||||
from functools import wraps
|
||||
from urllib.parse import unquote
|
||||
|
||||
# pylint: disable=wrong-import-position
|
||||
import cherrypy
|
||||
|
||||
from ..api.doc import SchemaInput, SchemaType
|
||||
from ..exceptions import PermissionNotValid, ScopeNotValid
|
||||
from ..plugins import PLUGIN_MANAGER
|
||||
from ..security import Permission, Scope
|
||||
@ -107,7 +108,12 @@ def EndpointDoc(description="", group="", parameters=None, responses=None): # n
|
||||
resp = {}
|
||||
if responses:
|
||||
for status_code, response_body in responses.items():
|
||||
resp[str(status_code)] = _split_parameters(response_body)
|
||||
schema_input = SchemaInput()
|
||||
schema_input.type = SchemaType.ARRAY if \
|
||||
isinstance(response_body, list) else SchemaType.OBJECT
|
||||
schema_input.params = _split_parameters(response_body)
|
||||
|
||||
resp[str(status_code)] = schema_input
|
||||
|
||||
def _wrapper(func):
|
||||
func.doc_info = {
|
||||
@ -662,7 +668,7 @@ class BaseController(object):
|
||||
def inner(*args, **kwargs):
|
||||
for key, value in kwargs.items():
|
||||
if isinstance(value, str):
|
||||
kwargs[key] = urllib.parse.unquote(value)
|
||||
kwargs[key] = unquote(value)
|
||||
|
||||
# Process method arguments.
|
||||
params = get_request_body_params(cherrypy.request)
|
||||
|
@ -2,11 +2,12 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Union
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
import cherrypy
|
||||
|
||||
from .. import mgr
|
||||
from ..api.doc import Schema, SchemaInput, SchemaType
|
||||
from . import ENDPOINT_MAP, BaseController, Controller, Endpoint, allow_empty_body
|
||||
|
||||
NO_DESCRIPTION_AVAILABLE = "*No description available*"
|
||||
@ -71,35 +72,35 @@ class Docs(BaseController):
|
||||
param_name = param['name']
|
||||
def_value = param['default'] if 'default' in param else None
|
||||
if param_name.startswith("is_"):
|
||||
return "boolean"
|
||||
return str(SchemaType.BOOLEAN)
|
||||
if "size" in param_name:
|
||||
return "integer"
|
||||
return str(SchemaType.INTEGER)
|
||||
if "count" in param_name:
|
||||
return "integer"
|
||||
return str(SchemaType.INTEGER)
|
||||
if "num" in param_name:
|
||||
return "integer"
|
||||
return str(SchemaType.INTEGER)
|
||||
if isinstance(def_value, bool):
|
||||
return "boolean"
|
||||
return str(SchemaType.BOOLEAN)
|
||||
if isinstance(def_value, int):
|
||||
return "integer"
|
||||
return "string"
|
||||
return str(SchemaType.INTEGER)
|
||||
return str(SchemaType.STRING)
|
||||
|
||||
@classmethod
|
||||
# isinstance doesn't work: input is always <type 'type'>.
|
||||
def _type_to_str(cls, type_as_type):
|
||||
""" Used if type is explicitly defined. """
|
||||
if type_as_type is str:
|
||||
type_as_str = 'string'
|
||||
type_as_str = str(SchemaType.STRING)
|
||||
elif type_as_type is int:
|
||||
type_as_str = 'integer'
|
||||
type_as_str = str(SchemaType.INTEGER)
|
||||
elif type_as_type is bool:
|
||||
type_as_str = 'boolean'
|
||||
type_as_str = str(SchemaType.BOOLEAN)
|
||||
elif type_as_type is list or type_as_type is tuple:
|
||||
type_as_str = 'array'
|
||||
type_as_str = str(SchemaType.ARRAY)
|
||||
elif type_as_type is float:
|
||||
type_as_str = 'number'
|
||||
type_as_str = str(SchemaType.NUMBER)
|
||||
else:
|
||||
type_as_str = 'object'
|
||||
type_as_str = str(SchemaType.OBJECT)
|
||||
return type_as_str
|
||||
|
||||
@classmethod
|
||||
@ -143,13 +144,17 @@ class Docs(BaseController):
|
||||
return parameters
|
||||
|
||||
@classmethod
|
||||
def _gen_schema_for_content(cls, params):
|
||||
def _gen_schema_for_content(cls, params: List[Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Generates information to the content-object in OpenAPI Spec.
|
||||
Used to for request body and responses.
|
||||
"""
|
||||
required_params = []
|
||||
properties = {}
|
||||
schema_type = SchemaType.OBJECT
|
||||
if isinstance(params, SchemaInput):
|
||||
schema_type = params.type
|
||||
params = params.params
|
||||
|
||||
for param in params:
|
||||
if param['required']:
|
||||
@ -159,12 +164,12 @@ class Docs(BaseController):
|
||||
if 'type' in param:
|
||||
props['type'] = cls._type_to_str(param['type'])
|
||||
if 'nested_params' in param:
|
||||
if props['type'] == 'array': # dict in array
|
||||
if props['type'] == str(SchemaType.ARRAY): # dict in array
|
||||
props['items'] = cls._gen_schema_for_content(param['nested_params'])
|
||||
else: # dict in dict
|
||||
props = cls._gen_schema_for_content(param['nested_params'])
|
||||
elif props['type'] == 'object': # e.g. [int]
|
||||
props['type'] = 'array'
|
||||
elif props['type'] == str(SchemaType.OBJECT): # e.g. [int]
|
||||
props['type'] = str(SchemaType.ARRAY)
|
||||
props['items'] = {'type': cls._type_to_str(param['type'][0])}
|
||||
else:
|
||||
props['type'] = cls._gen_type(param)
|
||||
@ -174,13 +179,10 @@ class Docs(BaseController):
|
||||
props['default'] = param['default']
|
||||
properties[param['name']] = props
|
||||
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': properties,
|
||||
}
|
||||
if required_params:
|
||||
schema['required'] = required_params
|
||||
return schema
|
||||
schema = Schema(schema_type=schema_type, properties=properties,
|
||||
required=required_params)
|
||||
|
||||
return schema.as_dict()
|
||||
|
||||
@classmethod
|
||||
def _gen_responses(cls, method, resp_object=None):
|
||||
@ -215,10 +217,11 @@ class Docs(BaseController):
|
||||
|
||||
if resp_object:
|
||||
for status_code, response_body in resp_object.items():
|
||||
resp[status_code].update({
|
||||
'content': {
|
||||
'application/json': {
|
||||
'schema': cls._gen_schema_for_content(response_body)}}})
|
||||
if status_code in resp:
|
||||
resp[status_code].update({
|
||||
'content': {
|
||||
'application/json': {
|
||||
'schema': cls._gen_schema_for_content(response_body)}}})
|
||||
|
||||
return resp
|
||||
|
||||
|
@ -74,7 +74,7 @@ class Rgw(BaseController):
|
||||
@ControllerDoc("RGW Daemon Management API", "RgwDaemon")
|
||||
class RgwDaemon(RESTController):
|
||||
@EndpointDoc("Display RGW Daemons",
|
||||
responses={200: RGW_DAEMON_SCHEMA})
|
||||
responses={200: [RGW_DAEMON_SCHEMA]})
|
||||
def list(self):
|
||||
# type: () -> List[dict]
|
||||
daemons = []
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
# # -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
from ..api.doc import SchemaType
|
||||
from ..controllers import ApiController, ControllerDoc, Endpoint, EndpointDoc, RESTController
|
||||
from ..controllers.docs import Docs
|
||||
from . import ControllerTestCase # pylint: disable=no-name-in-module
|
||||
@ -10,6 +11,8 @@ from . import ControllerTestCase # pylint: disable=no-name-in-module
|
||||
@ControllerDoc("Group description", group="FooGroup")
|
||||
@ApiController("/doctest/", secure=False)
|
||||
class DecoratedController(RESTController):
|
||||
RESOURCE_ID = 'doctest'
|
||||
|
||||
@EndpointDoc(
|
||||
description="Endpoint description",
|
||||
group="BarGroup",
|
||||
@ -17,12 +20,16 @@ class DecoratedController(RESTController):
|
||||
'parameter': (int, "Description of parameter"),
|
||||
},
|
||||
responses={
|
||||
200: {
|
||||
'resp': (str, 'Description of response')
|
||||
200: [{
|
||||
'my_prop': (str, '200 property desc.')
|
||||
}],
|
||||
202: {
|
||||
'my_prop': (str, '202 property desc.')
|
||||
},
|
||||
},
|
||||
)
|
||||
@Endpoint(json_response=False)
|
||||
@RESTController.Resource('PUT')
|
||||
def decorated_func(self, parameter):
|
||||
pass
|
||||
|
||||
@ -54,18 +61,48 @@ class DocDecoratorsTest(ControllerTestCase):
|
||||
class DocsTest(ControllerTestCase):
|
||||
@classmethod
|
||||
def setup_server(cls):
|
||||
cls.setup_controllers([Docs], "/test")
|
||||
cls.setup_controllers([DecoratedController, Docs], "/test")
|
||||
|
||||
def test_type_to_str(self):
|
||||
self.assertEqual(Docs()._type_to_str(str), "string")
|
||||
self.assertEqual(Docs()._type_to_str(str), str(SchemaType.STRING))
|
||||
self.assertEqual(Docs()._type_to_str(int), str(SchemaType.INTEGER))
|
||||
self.assertEqual(Docs()._type_to_str(bool), str(SchemaType.BOOLEAN))
|
||||
self.assertEqual(Docs()._type_to_str(list), str(SchemaType.ARRAY))
|
||||
self.assertEqual(Docs()._type_to_str(tuple), str(SchemaType.ARRAY))
|
||||
self.assertEqual(Docs()._type_to_str(float), str(SchemaType.NUMBER))
|
||||
self.assertEqual(Docs()._type_to_str(object), str(SchemaType.OBJECT))
|
||||
self.assertEqual(Docs()._type_to_str(None), str(SchemaType.OBJECT))
|
||||
|
||||
def test_gen_paths(self):
|
||||
outcome = Docs()._gen_paths(False)['/api/doctest//decorated_func/{parameter}']['get']
|
||||
outcome = Docs()._gen_paths(False)['/api/doctest//{doctest}/decorated_func']['put']
|
||||
self.assertIn('tags', outcome)
|
||||
self.assertIn('summary', outcome)
|
||||
self.assertIn('parameters', outcome)
|
||||
self.assertIn('responses', outcome)
|
||||
|
||||
expected_response_content = {
|
||||
'200': {
|
||||
'application/json': {
|
||||
'schema': {'type': 'array',
|
||||
'items': {'type': 'object', 'properties': {
|
||||
'my_prop': {
|
||||
'type': 'string',
|
||||
'description': '200 property desc.'}}},
|
||||
'required': ['my_prop']}}},
|
||||
'202': {
|
||||
'application/json': {
|
||||
'schema': {'type': 'object',
|
||||
'properties': {'my_prop': {
|
||||
'type': 'string',
|
||||
'description': '202 property desc.'}},
|
||||
'required': ['my_prop']}}
|
||||
}
|
||||
}
|
||||
# Check that a schema of type 'array' is received in the response.
|
||||
self.assertEqual(expected_response_content['200'], outcome['responses']['200']['content'])
|
||||
# Check that a schema of type 'object' is received in the response.
|
||||
self.assertEqual(expected_response_content['202'], outcome['responses']['202']['content'])
|
||||
|
||||
def test_gen_paths_all(self):
|
||||
paths = Docs()._gen_paths(False)
|
||||
for key in paths:
|
||||
|
@ -84,7 +84,7 @@ wrap_length = 80
|
||||
[pylint]
|
||||
# Allow similarity/code duplication detection
|
||||
jobs = 1
|
||||
dirs = . controllers plugins services tests
|
||||
dirs = . api controllers plugins services tests
|
||||
addopts = -rn --rcfile=.pylintrc --jobs={[pylint]jobs}
|
||||
|
||||
[rstlint]
|
||||
|
Loading…
Reference in New Issue
Block a user