doc.py 5.6KB

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