doc.py 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  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. jsonschema = schema2jsonschema(schema_class, spec=spec)
  52. for name, schema in jsonschema.get('properties', {}).items():
  53. method_operations.setdefault('parameters', []).append({
  54. 'in': 'path',
  55. 'name': name,
  56. 'required': name in jsonschema.get('required', []),
  57. 'type': schema['type']
  58. })
  59. if description.input_query:
  60. schema_class = type(description.input_query.wrapper.processor.schema)
  61. jsonschema = schema2jsonschema(schema_class, spec=spec)
  62. for name, schema in jsonschema.get('properties', {}).items():
  63. method_operations.setdefault('parameters', []).append({
  64. 'in': 'query',
  65. 'name': name,
  66. 'required': name in jsonschema.get('required', []),
  67. 'type': schema['type']
  68. })
  69. operations = {
  70. bottle_route.method.lower(): method_operations,
  71. }
  72. return operations
  73. class DocGenerator(object):
  74. def get_doc(
  75. self,
  76. controllers: typing.List[DecoratedController],
  77. app,
  78. ) -> dict:
  79. # TODO: Découper
  80. # TODO: bottle specific code !
  81. if not app:
  82. app = bottle.default_app()
  83. else:
  84. bottle.default_app.push(app)
  85. flatten = lambda l: [item for sublist in l for item in sublist]
  86. spec = APISpec(
  87. title='Swagger Petstore',
  88. version='1.0.0',
  89. plugins=[
  90. 'apispec.ext.bottle',
  91. 'apispec.ext.marshmallow',
  92. ],
  93. )
  94. schemas = []
  95. # parse schemas
  96. for controller in controllers:
  97. description = controller.description
  98. if description.input_body:
  99. schemas.append(type(
  100. description.input_body.wrapper.processor.schema
  101. ))
  102. if description.input_forms:
  103. schemas.append(type(
  104. description.input_forms.wrapper.processor.schema
  105. ))
  106. if description.output_body:
  107. schemas.append(type(
  108. description.output_body.wrapper.processor.schema
  109. ))
  110. for schema in set(schemas):
  111. spec.definition(schema.__name__, schema=schema)
  112. # add views
  113. # with app.test_request_context():
  114. paths = {}
  115. for controller in controllers:
  116. bottle_route = bottle_route_for_view(controller.reference, app)
  117. swagger_path = BOTTLE_RE_PATH_URL.sub(r'{\1}', bottle_route.rule)
  118. operations = bottle_generate_operations(
  119. spec,
  120. bottle_route,
  121. controller.description,
  122. )
  123. path = Path(path=swagger_path, operations=operations)
  124. if swagger_path in paths:
  125. paths[swagger_path].update(path)
  126. else:
  127. paths[swagger_path] = path
  128. spec.add_path(path)
  129. return spec.to_dict()
  130. # route_by_callbacks = []
  131. # routes = flatten(app.router.dyna_routes.values())
  132. # for path, path_regex, route, func_ in routes:
  133. # route_by_callbacks.append(route.callback)
  134. #
  135. # for description in self._controllers:
  136. # for path, path_regex, route, func_ in routes:
  137. # if route.callback == description.reference:
  138. # # TODO: use description to feed apispec
  139. # print(route.method, path, description)
  140. # continue