mirror of
https://github.com/dkmstr/openuds.git
synced 2025-03-20 06:50:23 +03:00
Refactor API documentation for clarity and consistency in method descriptions
This commit is contained in:
parent
d82e7dc838
commit
2d524fcdf2
@ -162,7 +162,7 @@ class Documentation(View):
|
||||
help_data.append(HelpInfo(f'{path}/{func.value.method}', func.value.text, method))
|
||||
case _:
|
||||
for method in node.methods:
|
||||
help_data.append(HelpInfo(path, node.help.text, method))
|
||||
help_data.append(HelpInfo(path, node.help.description, method))
|
||||
|
||||
for child in node.children:
|
||||
_process_node(child, child.help.path)
|
||||
|
@ -29,6 +29,7 @@
|
||||
"""
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import abc
|
||||
import typing
|
||||
import logging
|
||||
import codecs
|
||||
@ -53,7 +54,7 @@ if typing.TYPE_CHECKING:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Handler:
|
||||
class Handler(abc.ABC):
|
||||
"""
|
||||
REST requests handler base class
|
||||
"""
|
||||
@ -71,7 +72,7 @@ class Handler:
|
||||
|
||||
# For implementing help
|
||||
# A list of pairs of (path, help) for subpaths on this handler
|
||||
help_paths: typing.ClassVar[list[types.rest.HelpPath]] = []
|
||||
help_paths: typing.ClassVar[list[types.rest.HelpDoc]] = []
|
||||
help_text: typing.ClassVar[str] = 'No help available'
|
||||
|
||||
_request: 'ExtendedHttpRequestWithUser' # It's a modified HttpRequest
|
||||
|
@ -91,18 +91,14 @@ class Accounts(ModelHandler):
|
||||
def timemark(self, item: 'Model') -> typing.Any:
|
||||
"""
|
||||
API:
|
||||
Description:
|
||||
Generates a time mark associated with the account.
|
||||
This is useful to easily identify when the account data was last updated.
|
||||
(For example, one user enters an service, we get the usage data and "timemark" it, later read again
|
||||
and we can identify that all data before this timemark has already been processed)
|
||||
Generates a time mark associated with the account.
|
||||
This is useful to easily identify when the account data was last updated.
|
||||
(For example, one user enters an service, we get the usage data and "timemark" it, later read again
|
||||
and we can identify that all data before this timemark has already been processed)
|
||||
|
||||
Arguments:
|
||||
item: Account to timemark
|
||||
|
||||
Parameters:
|
||||
- item: Account
|
||||
Description of the item parameter
|
||||
|
||||
Response:
|
||||
200: All fine, no data returned
|
||||
"""
|
||||
item = ensure.is_instance(item, Account)
|
||||
item.time_mark = datetime.datetime.now()
|
||||
@ -114,15 +110,7 @@ class Accounts(ModelHandler):
|
||||
Api documentation for the method. From here, will be used by the documentation generator
|
||||
Always starts with API:
|
||||
API:
|
||||
Description:
|
||||
Clears all usage associated with the account
|
||||
|
||||
Parameters:
|
||||
- item: Account
|
||||
Description of the item parameter
|
||||
|
||||
Response:
|
||||
200: All fine, no data returned
|
||||
Clears all usage associated with the account
|
||||
"""
|
||||
item = ensure.is_instance(item, Account)
|
||||
self.ensure_has_access(item, uds.core.types.permissions.PermissionType.MANAGEMENT)
|
||||
|
@ -201,13 +201,7 @@ class Authenticators(ModelHandler):
|
||||
def search(self, item: 'Model') -> list[types.rest.ItemDictType]:
|
||||
"""
|
||||
API:
|
||||
Description:
|
||||
Search for users or groups in this authenticator
|
||||
|
||||
|
||||
|
||||
Args:
|
||||
item: Authenticator instance
|
||||
Search for users or groups in this authenticator
|
||||
"""
|
||||
item = ensure.is_instance(item, Authenticator)
|
||||
self.ensure_has_access(item, types.permissions.PermissionType.READ)
|
||||
|
@ -45,12 +45,7 @@ logger = logging.getLogger(__name__)
|
||||
class Config(Handler):
|
||||
"""
|
||||
API:
|
||||
Description: Get or update UDS configuration
|
||||
|
||||
Actions:
|
||||
- GET: Returns the configuration values
|
||||
- PUT: Updates the configuration values
|
||||
|
||||
Get or update UDS configuration
|
||||
"""
|
||||
|
||||
min_access_role = consts.UserRole.ADMIN
|
||||
|
@ -44,8 +44,7 @@ logger = logging.getLogger(__name__)
|
||||
class Callback(Handler):
|
||||
"""
|
||||
API:
|
||||
Description:
|
||||
Executes a callback from the GUI. Internal use, not intended to be called from outside.
|
||||
Executes a callback from the GUI. Internal use, not intended to be called from outside.
|
||||
"""
|
||||
path = 'gui'
|
||||
|
||||
|
@ -292,11 +292,7 @@ class MetaPools(ModelHandler):
|
||||
def set_fallback_access(self, item: MetaPool) -> typing.Any:
|
||||
"""
|
||||
API:
|
||||
Description:
|
||||
Sets the fallback access for a metapool
|
||||
|
||||
Response:
|
||||
200: All fine, no data returned
|
||||
Sets the fallback access for a metapool
|
||||
"""
|
||||
self.ensure_has_access(item, types.permissions.PermissionType.MANAGEMENT)
|
||||
|
||||
|
@ -47,7 +47,7 @@ class Stats(Handler):
|
||||
min_access_role = consts.UserRole.ADMIN
|
||||
|
||||
help_paths = [
|
||||
types.rest.HelpPath('', 'Returns the last day usage statistics for all authenticators'),
|
||||
types.rest.HelpDoc('', 'Returns the last day usage statistics for all authenticators'),
|
||||
]
|
||||
help_text = 'Provides access to usage statistics'
|
||||
|
||||
|
@ -146,15 +146,15 @@ class System(Handler):
|
||||
min_access_role = consts.UserRole.STAFF
|
||||
|
||||
help_paths = [
|
||||
types.rest.HelpPath('', ''),
|
||||
types.rest.HelpPath('stats/assigned', ''),
|
||||
types.rest.HelpPath('stats/inuse', ''),
|
||||
types.rest.HelpPath('stats/cached', ''),
|
||||
types.rest.HelpPath('stats/complete', ''),
|
||||
types.rest.HelpPath('stats/assigned/<uuuid>', 'Get service pool assigned stats'),
|
||||
types.rest.HelpPath('stats/inuse/<uuid>', 'Get service pool in use stats'),
|
||||
types.rest.HelpPath('stats/cached/<uuid>', 'Get service pool cached stats'),
|
||||
types.rest.HelpPath('stats/complete/<uuid>', 'Get service pool complete stats'),
|
||||
types.rest.HelpDoc('', ''),
|
||||
types.rest.HelpDoc('stats/assigned', ''),
|
||||
types.rest.HelpDoc('stats/inuse', ''),
|
||||
types.rest.HelpDoc('stats/cached', ''),
|
||||
types.rest.HelpDoc('stats/complete', ''),
|
||||
types.rest.HelpDoc('stats/assigned/<uuuid>', 'Get service pool assigned stats'),
|
||||
types.rest.HelpDoc('stats/inuse/<uuid>', 'Get service pool in use stats'),
|
||||
types.rest.HelpDoc('stats/cached/<uuid>', 'Get service pool cached stats'),
|
||||
types.rest.HelpDoc('stats/complete/<uuid>', 'Get service pool complete stats'),
|
||||
]
|
||||
help_text = 'Provides system information. Must be admin to access this'
|
||||
|
||||
|
@ -124,8 +124,7 @@ class TunnelServers(DetailHandler):
|
||||
def maintenance(self, parent: 'Model', id: str) -> typing.Any:
|
||||
"""
|
||||
API:
|
||||
Description:
|
||||
Custom method that swaps maintenance mode state for a tunnel server
|
||||
Custom method that swaps maintenance mode state for a tunnel server
|
||||
|
||||
"""
|
||||
parent = ensure.is_instance(parent, models.ServerGroup)
|
||||
|
@ -295,14 +295,7 @@ class Users(DetailHandler):
|
||||
def services_pools(self, parent: 'Model', item: str) -> list[dict[str, typing.Any]]:
|
||||
"""
|
||||
API:
|
||||
Description:
|
||||
Returns the service pools assigned to a user
|
||||
|
||||
Parameters:
|
||||
- uuid: User
|
||||
|
||||
Response:
|
||||
- 200: A list of service pools assigned to the user
|
||||
Returns the service pools assigned to a user
|
||||
"""
|
||||
parent = ensure.is_instance(parent, Authenticator)
|
||||
uuid = process_uuid(item)
|
||||
|
@ -40,7 +40,7 @@ import typing
|
||||
from django.http import HttpResponse
|
||||
from django.utils.functional import Promise as DjangoPromise
|
||||
|
||||
from uds.core import consts
|
||||
from uds.core import consts, types
|
||||
|
||||
from .utils import to_incremental_json
|
||||
|
||||
@ -112,6 +112,10 @@ class ContentProcessor:
|
||||
if obj is None or isinstance(obj, (bool, int, float, str)):
|
||||
return obj
|
||||
|
||||
# If we are a typed response...
|
||||
if isinstance(obj, types.rest.TypedResponse):
|
||||
return ContentProcessor.process_for_render(obj.as_dict())
|
||||
|
||||
if isinstance(obj, DjangoPromise):
|
||||
return str(obj) # This is for translations
|
||||
|
||||
|
@ -40,6 +40,51 @@ if typing.TYPE_CHECKING:
|
||||
from uds.REST.handlers import Handler
|
||||
|
||||
|
||||
# TypedResponse related.
|
||||
# Typed responses are used to define the type of the response that a method will return.
|
||||
# This allow us to "describe" it later on the documentation, and also to check that the
|
||||
# response is correct (and also to generate the response in the correct format)
|
||||
class TypedResponse(abc.ABC):
|
||||
def as_dict(self) -> dict[str, typing.Any]:
|
||||
# If we are a dataclass
|
||||
if dataclasses.is_dataclass(self):
|
||||
return dataclasses.asdict(self)
|
||||
# If we are a dict
|
||||
if isinstance(self, dict):
|
||||
return self
|
||||
|
||||
raise Exception(f'Cannot convert {self} to dict')
|
||||
|
||||
@classmethod
|
||||
def as_help(cls: type) -> dict[str, typing.Any]:
|
||||
"""
|
||||
Returns a representation, as json, of the response type to be used on documentation
|
||||
|
||||
For this, we build a dict of "name": "<type>" for each field of the response and returns it
|
||||
Note that we support nested dataclasses and dicts, but not lists
|
||||
"""
|
||||
CLASS_REPR: dict[typing.Any, str] = {
|
||||
str: '<string>',
|
||||
int: '<integer>',
|
||||
float: '<float>',
|
||||
bool: '<boolean>',
|
||||
dict: '<dict>',
|
||||
list: '<list>',
|
||||
typing.Any: '<any>',
|
||||
}
|
||||
|
||||
def _as_help(obj: typing.Any) -> typing.Union[str, dict[str, typing.Any]]:
|
||||
if dataclasses.is_dataclass(obj):
|
||||
return {field.name: _as_help(field.type) for field in dataclasses.fields(obj)}
|
||||
if isinstance(obj, dict):
|
||||
return {k: str(_as_help(v)) for k, v in typing.cast(dict[str, typing.Any], obj).items()}
|
||||
|
||||
return CLASS_REPR.get(obj, str(obj))
|
||||
|
||||
return {field.name: _as_help(field.type) for field in dataclasses.fields(cls)}
|
||||
|
||||
|
||||
# Type related definitions
|
||||
TypeInfoDict = dict[str, typing.Any] # Alias for type info dict
|
||||
|
||||
|
||||
@ -121,40 +166,78 @@ API_RE = re.compile(r'(?ms)^\s*API:\s*$')
|
||||
|
||||
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class HelpPath:
|
||||
class HelpDoc:
|
||||
"""
|
||||
Help helper class
|
||||
"""
|
||||
@dataclasses.dataclass
|
||||
class ArgumentInfo:
|
||||
name: str
|
||||
type: str
|
||||
description: str
|
||||
|
||||
|
||||
path: str
|
||||
text: str
|
||||
description: str
|
||||
arguments: list[ArgumentInfo] = dataclasses.field(default_factory=list)
|
||||
# Result is always a json ressponse, so we can describe it as a dict
|
||||
# Note that this dict can be nested
|
||||
returns: dict[str, typing.Any] = dataclasses.field(default_factory=dict)
|
||||
|
||||
def __init__(self, path: str, help: str):
|
||||
def __init__(
|
||||
self,
|
||||
path: str,
|
||||
help: str,
|
||||
*,
|
||||
arguments: typing.Optional[list[ArgumentInfo]] = None,
|
||||
returns: typing.Optional[dict[str, typing.Any]] = None,
|
||||
) -> None:
|
||||
self.path = path
|
||||
self.text = HelpPath.process_help(help)
|
||||
self.description = help
|
||||
self.arguments = arguments or []
|
||||
self.returns = returns or {}
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.path)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, HelpPath):
|
||||
if not isinstance(other, HelpDoc):
|
||||
return False
|
||||
return self.path == other.path
|
||||
|
||||
def as_str(self) -> str:
|
||||
return f'{self.path} - {self.description}'
|
||||
|
||||
@property
|
||||
def is_empty(self) -> bool:
|
||||
return self.path == '' and self.text == ''
|
||||
return self.path == '' and self.description == ''
|
||||
|
||||
@staticmethod
|
||||
def process_help(help: str) -> str:
|
||||
def _process_help(self, help: str, annotations: typing.Optional[dict[str, typing.Any]] = None) -> None:
|
||||
"""
|
||||
Processes the help string, removing leading and trailing spaces
|
||||
"""
|
||||
self.description = ''
|
||||
self.arguments = []
|
||||
self.returns = {}
|
||||
|
||||
match = API_RE.search(help)
|
||||
if match:
|
||||
return help[match.end() :].strip()
|
||||
self.description = help[: match.start()].strip()
|
||||
|
||||
return ''
|
||||
if annotations:
|
||||
if 'return' in annotations and issubclass(annotations['return'], TypedResponse):
|
||||
self.returns = annotations['return'].as_help()
|
||||
|
||||
@staticmethod
|
||||
def from_typed_response(path: str, help: str, TR: type[TypedResponse]) -> 'HelpDoc':
|
||||
"""
|
||||
Returns a HelpDoc from a TypedResponse class
|
||||
"""
|
||||
return HelpDoc(
|
||||
path=path,
|
||||
help=help,
|
||||
returns=TR.as_help(),
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@ -164,7 +247,7 @@ class HelpNode:
|
||||
DETAIL = 'detail'
|
||||
CUSTOM = 'custom'
|
||||
PATH = 'path'
|
||||
|
||||
|
||||
class Methods(enum.StrEnum):
|
||||
GET = 'GET'
|
||||
POST = 'POST'
|
||||
@ -172,7 +255,7 @@ class HelpNode:
|
||||
DELETE = 'DELETE'
|
||||
PATCH = 'PATCH'
|
||||
|
||||
help: HelpPath
|
||||
help: HelpDoc
|
||||
children: list['HelpNode'] # Children nodes
|
||||
kind: Type
|
||||
methods: set[Methods] = dataclasses.field(default_factory=lambda: {HelpNode.Methods.GET})
|
||||
@ -183,7 +266,7 @@ class HelpNode:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, HelpNode):
|
||||
return self.help.path == other.help.path and self.methods == other.methods
|
||||
if not isinstance(other, HelpPath):
|
||||
if not isinstance(other, HelpDoc):
|
||||
return False
|
||||
|
||||
return self.help.path == other.path
|
||||
@ -256,10 +339,14 @@ class HandlerNode:
|
||||
# Method is a Me CustomModelMethod,
|
||||
# We access the __doc__ of the function inside the handler with method.name
|
||||
doc = getattr(self.handler, method.name).__doc__ or ''
|
||||
path = f'{self.full_path()}/{method.name}' if not method.needs_parent else f'{self.full_path()}/<uuid>/{method.name}'
|
||||
path = (
|
||||
f'{self.full_path()}/{method.name}'
|
||||
if not method.needs_parent
|
||||
else f'{self.full_path()}/<uuid>/{method.name}'
|
||||
)
|
||||
custom_help.add(
|
||||
HelpNode(
|
||||
HelpPath(path=path, help=doc),
|
||||
HelpDoc(path=path, help=doc),
|
||||
[],
|
||||
HelpNode.Type.CUSTOM,
|
||||
)
|
||||
@ -270,7 +357,7 @@ class HandlerNode:
|
||||
for method_name, method_class in self.handler.detail.items():
|
||||
custom_help.add(
|
||||
HelpNode(
|
||||
HelpPath(path=self.full_path() + '/' + method_name, help=''),
|
||||
HelpDoc(path=self.full_path() + '/' + method_name, help=''),
|
||||
[],
|
||||
HelpNode.Type.DETAIL,
|
||||
)
|
||||
@ -282,7 +369,7 @@ class HandlerNode:
|
||||
doc = getattr(method_class, detail_method).__doc__ or ''
|
||||
custom_help.add(
|
||||
HelpNode(
|
||||
HelpPath(
|
||||
HelpDoc(
|
||||
path=self.full_path()
|
||||
+ '/<uuid>/'
|
||||
+ method_name
|
||||
@ -297,9 +384,9 @@ class HandlerNode:
|
||||
|
||||
custom_help |= {
|
||||
HelpNode(
|
||||
HelpPath(
|
||||
HelpDoc(
|
||||
path=self.full_path() + '/' + help_info.path,
|
||||
help=help_info.text,
|
||||
help=help_info.description,
|
||||
),
|
||||
[],
|
||||
help_node_type,
|
||||
@ -310,7 +397,7 @@ class HandlerNode:
|
||||
custom_help |= {child.help_node() for child in self.children.values()}
|
||||
|
||||
return HelpNode(
|
||||
help=HelpPath(path=self.full_path(), help=self.handler.__doc__ or ''),
|
||||
help=HelpDoc(path=self.full_path(), help=self.handler.__doc__ or ''),
|
||||
children=list(custom_help),
|
||||
kind=help_node_type,
|
||||
)
|
||||
|
0
server/tests/REST/test_documentation.py
Normal file
0
server/tests/REST/test_documentation.py
Normal file
26
server/tests/core/types/__init__.py
Normal file
26
server/tests/core/types/__init__.py
Normal file
@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (c) 2025 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
26
server/tests/core/types/rest/__init__.py
Normal file
26
server/tests/core/types/rest/__init__.py
Normal file
@ -0,0 +1,26 @@
|
||||
#
|
||||
# Copyright (c) 2025 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
102
server/tests/core/types/rest/test_helpdoc.py
Normal file
102
server/tests/core/types/rest/test_helpdoc.py
Normal file
@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2022 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import dataclasses
|
||||
import logging
|
||||
from unittest import TestCase
|
||||
|
||||
from uds.core.types import rest
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestHelpDoc(TestCase):
|
||||
def test_helpdoc_basic(self) -> None:
|
||||
h = rest.HelpDoc('/path', 'help_text')
|
||||
|
||||
self.assertEqual(h.path, '/path')
|
||||
self.assertEqual(h.description, 'help_text')
|
||||
self.assertEqual(h.arguments, [])
|
||||
|
||||
def test_helpdoc_with_args(self) -> None:
|
||||
arguments = [
|
||||
rest.HelpDoc.ArgumentInfo('arg1', 'arg1_type', 'arg1_description'),
|
||||
rest.HelpDoc.ArgumentInfo('arg2', 'arg2_type', 'arg2_description'),
|
||||
]
|
||||
h = rest.HelpDoc(
|
||||
'/path',
|
||||
'help_text',
|
||||
arguments=arguments,
|
||||
)
|
||||
|
||||
self.assertEqual(h.path, '/path')
|
||||
self.assertEqual(h.description, 'help_text')
|
||||
self.assertEqual(h.arguments, arguments)
|
||||
|
||||
def test_helpdoc_with_args_and_return(self) -> None:
|
||||
arguments = [
|
||||
rest.HelpDoc.ArgumentInfo('arg1', 'arg1_type', 'arg1_description'),
|
||||
rest.HelpDoc.ArgumentInfo('arg2', 'arg2_type', 'arg2_description'),
|
||||
]
|
||||
returns = {
|
||||
'name': 'return_name',
|
||||
}
|
||||
h = rest.HelpDoc(
|
||||
'/path',
|
||||
'help_text',
|
||||
arguments=arguments,
|
||||
returns=returns,
|
||||
)
|
||||
|
||||
self.assertEqual(h.path, '/path')
|
||||
self.assertEqual(h.description, 'help_text')
|
||||
self.assertEqual(h.arguments, arguments)
|
||||
self.assertEqual(h.returns, returns)
|
||||
|
||||
|
||||
def test_help_doc_from_typed_response(self) -> None:
|
||||
@dataclasses.dataclass
|
||||
class TestResponse(rest.TypedResponse):
|
||||
name: str = 'test_name'
|
||||
age: int = 0
|
||||
money: float = 0.0
|
||||
|
||||
h = rest.HelpDoc.from_typed_response('path', 'help', TestResponse)
|
||||
|
||||
self.assertEqual(h.path, 'path')
|
||||
self.assertEqual(h.description, 'help')
|
||||
self.assertEqual(h.arguments, [])
|
||||
self.assertEqual(h.returns, {
|
||||
'name': '<string>',
|
||||
'age': '<integer>',
|
||||
'money': '<float>',
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user