doc.py 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. # -*- coding: utf-8 -*-
  2. import typing
  3. from apispec import APISpec
  4. from apispec import Path
  5. from apispec.ext.marshmallow.swagger import schema2jsonschema
  6. from hapic.context import ContextInterface
  7. from hapic.context import RouteRepresentation
  8. from hapic.decorator import DecoratedController
  9. from hapic.description import ControllerDescription
  10. def bottle_generate_operations(
  11. spec,
  12. route: RouteRepresentation,
  13. description: ControllerDescription,
  14. ):
  15. method_operations = dict()
  16. # schema based
  17. if description.input_body:
  18. schema_class = type(description.input_body.wrapper.processor.schema)
  19. method_operations.setdefault('parameters', []).append({
  20. 'in': 'body',
  21. 'name': 'body',
  22. 'schema': {
  23. '$ref': '#/definitions/{}'.format(schema_class.__name__)
  24. }
  25. })
  26. if description.output_body:
  27. schema_class = type(description.output_body.wrapper.processor.schema)
  28. method_operations.setdefault('responses', {})\
  29. [int(description.output_body.wrapper.default_http_code)] = {
  30. 'description': int(description.output_body.wrapper.default_http_code), # nopep8
  31. 'schema': {
  32. '$ref': '#/definitions/{}'.format(schema_class.__name__)
  33. }
  34. }
  35. if description.output_file:
  36. method_operations.setdefault('produces', []).extend(
  37. description.output_file.wrapper.output_types
  38. )
  39. method_operations.setdefault('responses', {})\
  40. [int(description.output_file.wrapper.default_http_code)] = {
  41. 'description': int(description.output_file.wrapper.default_http_code), # nopep8
  42. }
  43. if description.errors:
  44. for error in description.errors:
  45. schema_class = type(error.wrapper.schema)
  46. method_operations.setdefault('responses', {})\
  47. [int(error.wrapper.http_code)] = {
  48. 'description': int(error.wrapper.http_code),
  49. 'schema': {
  50. '$ref': '#/definitions/{}'.format(schema_class.__name__) # nopep8
  51. }
  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. if description.input_files:
  76. method_operations.setdefault('consumes', []).append('multipart/form-data')
  77. for field_name, field in description.input_files.wrapper.processor.schema.fields.items():
  78. method_operations.setdefault('parameters', []).append({
  79. 'in': 'formData',
  80. 'name': field_name,
  81. 'required': field.required,
  82. 'type': 'file',
  83. })
  84. if description.tags:
  85. method_operations['tags'] = description.tags
  86. operations = {
  87. route.method.lower(): method_operations,
  88. }
  89. return operations
  90. class DocGenerator(object):
  91. def get_doc(
  92. self,
  93. controllers: typing.List[DecoratedController],
  94. context: ContextInterface,
  95. title: str='',
  96. description: str='',
  97. ) -> dict:
  98. """
  99. Generate an OpenApi 2.0 documentation. Th given context will be used
  100. to found controllers matching with given DecoratedController.
  101. :param controllers: List of DecoratedController to match with context
  102. controllers
  103. :param context: a context instance
  104. :param title: The generated doc title
  105. :param description: The generated doc description
  106. :return: a apispec documentation dict
  107. """
  108. spec = APISpec(
  109. title=title,
  110. info=dict(description=description),
  111. version='1.0.0',
  112. plugins=(
  113. 'apispec.ext.marshmallow',
  114. ),
  115. schema_name_resolver=generate_schema_name,
  116. )
  117. schemas = []
  118. # parse schemas
  119. for controller in controllers:
  120. description = controller.description
  121. if description.input_body:
  122. schemas.append(type(
  123. description.input_body.wrapper.processor.schema
  124. ))
  125. if description.input_forms:
  126. schemas.append(type(
  127. description.input_forms.wrapper.processor.schema
  128. ))
  129. if description.output_body:
  130. schemas.append(type(
  131. description.output_body.wrapper.processor.schema
  132. ))
  133. if description.errors:
  134. for error in description.errors:
  135. schemas.append(type(error.wrapper.schema))
  136. for schema in set(schemas):
  137. spec.definition(schema.__name__, schema=schema)
  138. # add views
  139. # with app.test_request_context():
  140. paths = {}
  141. for controller in controllers:
  142. route = context.find_route(controller)
  143. swagger_path = context.get_swagger_path(route.rule)
  144. operations = bottle_generate_operations(
  145. spec,
  146. route,
  147. controller.description,
  148. )
  149. # TODO BS 20171114: TMP code waiting refact of doc
  150. doc_string = controller.reference.get_doc_string()
  151. if doc_string:
  152. for method in operations.keys():
  153. operations[method]['description'] = doc_string
  154. path = Path(path=swagger_path, operations=operations)
  155. if swagger_path in paths:
  156. paths[swagger_path].update(path)
  157. else:
  158. paths[swagger_path] = path
  159. spec.add_path(path)
  160. return spec.to_dict()
  161. # TODO BS 20171109: Must take care of already existing definition names
  162. def generate_schema_name(schema):
  163. return schema.__name__