doc.py 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. # -*- coding: utf-8 -*-
  2. import re
  3. import typing
  4. import bottle
  5. from apispec import APISpec
  6. from apispec import Path
  7. from apispec.ext.marshmallow.swagger import schema2jsonschema
  8. from hapic.decorator import DecoratedController
  9. from hapic.decorator import DECORATION_ATTRIBUTE_NAME
  10. # Bottle regular expression to locate url parameters
  11. from hapic.description import ControllerDescription
  12. BOTTLE_RE_PATH_URL = re.compile(r'<(?:[^:<>]+:)?([^<>]+)>')
  13. def bottle_route_for_view(
  14. decorated_controller: DecoratedController,
  15. app: bottle.Bottle,
  16. ):
  17. for route in app.routes:
  18. route_token = getattr(
  19. route.callback,
  20. DECORATION_ATTRIBUTE_NAME,
  21. None,
  22. )
  23. if route_token == decorated_controller.token:
  24. return route
  25. # TODO BS 20171010: specialize exception
  26. # TODO BS 20171010: Raise exception or print error ?
  27. raise Exception(
  28. 'Decorated route "{}" was not found in bottle routes'.format(
  29. decorated_controller.name,
  30. )
  31. )
  32. def bottle_generate_operations(
  33. spec,
  34. bottle_route: bottle.Route,
  35. description: ControllerDescription,
  36. ):
  37. method_operations = dict()
  38. # schema based
  39. if description.input_body:
  40. schema_class = type(description.input_body.wrapper.processor.schema)
  41. method_operations.setdefault('parameters', []).append({
  42. 'in': 'body',
  43. 'name': 'body',
  44. 'schema': {
  45. '$ref': '#/definitions/{}'.format(schema_class.__name__)
  46. }
  47. })
  48. if description.output_body:
  49. schema_class = type(description.output_body.wrapper.processor.schema)
  50. method_operations.setdefault('responses', {})\
  51. [int(description.output_body.wrapper.default_http_code)] = {
  52. 'description': str(description.output_body.wrapper.default_http_code), # nopep8
  53. 'schema': {
  54. '$ref': '#/definitions/{}'.format(schema_class.__name__)
  55. }
  56. }
  57. if description.errors:
  58. for error in description.errors:
  59. method_operations.setdefault('responses', {})\
  60. [int(error.wrapper.http_code)] = {
  61. 'description': str(error.wrapper.http_code),
  62. }
  63. # jsonschema based
  64. if description.input_path:
  65. schema_class = type(description.input_path.wrapper.processor.schema)
  66. # TODO: look schema2parameters ?
  67. jsonschema = schema2jsonschema(schema_class, spec=spec)
  68. for name, schema in jsonschema.get('properties', {}).items():
  69. method_operations.setdefault('parameters', []).append({
  70. 'in': 'path',
  71. 'name': name,
  72. 'required': name in jsonschema.get('required', []),
  73. 'type': schema['type']
  74. })
  75. if description.input_query:
  76. schema_class = type(description.input_query.wrapper.processor.schema)
  77. jsonschema = schema2jsonschema(schema_class, spec=spec)
  78. for name, schema in jsonschema.get('properties', {}).items():
  79. method_operations.setdefault('parameters', []).append({
  80. 'in': 'query',
  81. 'name': name,
  82. 'required': name in jsonschema.get('required', []),
  83. 'type': schema['type']
  84. })
  85. operations = {
  86. bottle_route.method.lower(): method_operations,
  87. }
  88. return operations
  89. class DocGenerator(object):
  90. def get_doc(
  91. self,
  92. controllers: typing.List[DecoratedController],
  93. app,
  94. ) -> dict:
  95. # TODO: Découper
  96. # TODO: bottle specific code !
  97. if not app:
  98. app = bottle.default_app()
  99. else:
  100. bottle.default_app.push(app)
  101. flatten = lambda l: [item for sublist in l for item in sublist]
  102. spec = APISpec(
  103. title='Swagger Petstore',
  104. version='1.0.0',
  105. plugins=[
  106. 'apispec.ext.bottle',
  107. 'apispec.ext.marshmallow',
  108. ],
  109. )
  110. schemas = []
  111. # parse schemas
  112. for controller in controllers:
  113. description = controller.description
  114. if description.input_body:
  115. schemas.append(type(
  116. description.input_body.wrapper.processor.schema
  117. ))
  118. if description.input_forms:
  119. schemas.append(type(
  120. description.input_forms.wrapper.processor.schema
  121. ))
  122. if description.output_body:
  123. schemas.append(type(
  124. description.output_body.wrapper.processor.schema
  125. ))
  126. for schema in set(schemas):
  127. spec.definition(schema.__name__, schema=schema)
  128. # add views
  129. # with app.test_request_context():
  130. paths = {}
  131. for controller in controllers:
  132. bottle_route = bottle_route_for_view(controller, app)
  133. swagger_path = BOTTLE_RE_PATH_URL.sub(r'{\1}', bottle_route.rule)
  134. operations = bottle_generate_operations(
  135. spec,
  136. bottle_route,
  137. controller.description,
  138. )
  139. path = Path(path=swagger_path, operations=operations)
  140. if swagger_path in paths:
  141. paths[swagger_path].update(path)
  142. else:
  143. paths[swagger_path] = path
  144. spec.add_path(path)
  145. return spec.to_dict()
  146. # route_by_callbacks = []
  147. # routes = flatten(app.router.dyna_routes.values())
  148. # for path, path_regex, route, func_ in routes:
  149. # route_by_callbacks.append(route.callback)
  150. #
  151. # for description in self._controllers:
  152. # for path, path_regex, route, func_ in routes:
  153. # if route.callback == description.reference:
  154. # # TODO: use description to feed apispec
  155. # print(route.method, path, description)
  156. # continue