doc.py 6.1KB

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