doc.py 6.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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': str(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': str(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': str(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. operations = {
  85. route.method.lower(): method_operations,
  86. }
  87. return operations
  88. class DocGenerator(object):
  89. def get_doc(
  90. self,
  91. controllers: typing.List[DecoratedController],
  92. context: ContextInterface,
  93. ) -> dict:
  94. spec = APISpec(
  95. title='Swagger Petstore',
  96. version='1.0.0',
  97. plugins=[
  98. # 'apispec.ext.bottle',
  99. 'apispec.ext.marshmallow',
  100. ],
  101. schema_name_resolver=generate_schema_name,
  102. )
  103. schemas = []
  104. # parse schemas
  105. for controller in controllers:
  106. description = controller.description
  107. if description.input_body:
  108. schemas.append(type(
  109. description.input_body.wrapper.processor.schema
  110. ))
  111. if description.input_forms:
  112. schemas.append(type(
  113. description.input_forms.wrapper.processor.schema
  114. ))
  115. if description.output_body:
  116. schemas.append(type(
  117. description.output_body.wrapper.processor.schema
  118. ))
  119. if description.errors:
  120. for error in description.errors:
  121. schemas.append(type(error.wrapper.schema))
  122. for schema in set(schemas):
  123. spec.definition(schema.__name__, schema=schema)
  124. # add views
  125. # with app.test_request_context():
  126. paths = {}
  127. for controller in controllers:
  128. route = context.find_route(controller)
  129. swagger_path = context.get_swagger_path(route.rule)
  130. operations = bottle_generate_operations(
  131. spec,
  132. route,
  133. controller.description,
  134. )
  135. # TODO BS 20171114: TMP code waiting refact of doc
  136. doc_string = controller.reference.get_doc_string()
  137. if doc_string:
  138. for method in operations.keys():
  139. operations[method]['description'] = doc_string
  140. path = Path(path=swagger_path, operations=operations)
  141. if swagger_path in paths:
  142. paths[swagger_path].update(path)
  143. else:
  144. paths[swagger_path] = path
  145. spec.add_path(path)
  146. return spec.to_dict()
  147. # TODO BS 20171109: Must take care of already existing definition names
  148. def generate_schema_name(schema):
  149. return schema.__name__