doc.py 5.5KB

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