doc.py 7.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. # -*- coding: utf-8 -*-
  2. import json
  3. import typing
  4. import yaml
  5. from apispec import APISpec
  6. from apispec import Path
  7. from apispec.ext.marshmallow.swagger import schema2jsonschema
  8. from hapic.context import ContextInterface
  9. from hapic.context import RouteRepresentation
  10. from hapic.decorator import DecoratedController
  11. from hapic.description import ControllerDescription
  12. def bottle_generate_operations(
  13. spec,
  14. route: RouteRepresentation,
  15. description: ControllerDescription,
  16. ):
  17. method_operations = dict()
  18. # schema based
  19. if description.input_body:
  20. schema_class = type(description.input_body.wrapper.processor.schema)
  21. method_operations.setdefault('parameters', []).append({
  22. 'in': 'body',
  23. 'name': 'body',
  24. 'schema': {
  25. '$ref': '#/definitions/{}'.format(schema_class.__name__)
  26. }
  27. })
  28. if description.output_body:
  29. schema_class = type(description.output_body.wrapper.processor.schema)
  30. method_operations.setdefault('responses', {})\
  31. [int(description.output_body.wrapper.default_http_code)] = {
  32. 'description': str(int(description.output_body.wrapper.default_http_code)), # nopep8
  33. 'schema': {
  34. '$ref': '#/definitions/{}'.format(schema_class.__name__)
  35. }
  36. }
  37. if description.output_file:
  38. method_operations.setdefault('produces', []).extend(
  39. description.output_file.wrapper.output_types
  40. )
  41. method_operations.setdefault('responses', {})\
  42. [int(description.output_file.wrapper.default_http_code)] = {
  43. 'description': str(int(description.output_file.wrapper.default_http_code)), # nopep8
  44. }
  45. if description.errors:
  46. for error in description.errors:
  47. schema_class = type(error.wrapper.error_builder)
  48. method_operations.setdefault('responses', {})\
  49. [int(error.wrapper.http_code)] = {
  50. 'description': str(int(error.wrapper.http_code)),
  51. 'schema': {
  52. '$ref': '#/definitions/{}'.format(schema_class.__name__) # nopep8
  53. }
  54. }
  55. # jsonschema based
  56. if description.input_path:
  57. schema_class = type(description.input_path.wrapper.processor.schema)
  58. # TODO: look schema2parameters ?
  59. jsonschema = schema2jsonschema(schema_class, spec=spec)
  60. for name, schema in jsonschema.get('properties', {}).items():
  61. method_operations.setdefault('parameters', []).append({
  62. 'in': 'path',
  63. 'name': name,
  64. 'required': name in jsonschema.get('required', []),
  65. 'type': schema['type']
  66. })
  67. if description.input_query:
  68. schema_class = type(description.input_query.wrapper.processor.schema)
  69. jsonschema = schema2jsonschema(schema_class, spec=spec)
  70. for name, schema in jsonschema.get('properties', {}).items():
  71. method_operations.setdefault('parameters', []).append({
  72. 'in': 'query',
  73. 'name': name,
  74. 'required': name in jsonschema.get('required', []),
  75. 'type': schema['type']
  76. })
  77. if description.input_files:
  78. method_operations.setdefault('consumes', []).append('multipart/form-data')
  79. for field_name, field in description.input_files.wrapper.processor.schema.fields.items():
  80. method_operations.setdefault('parameters', []).append({
  81. 'in': 'formData',
  82. 'name': field_name,
  83. 'required': field.required,
  84. 'type': 'file',
  85. })
  86. if description.tags:
  87. method_operations['tags'] = description.tags
  88. operations = {
  89. route.method.lower(): method_operations,
  90. }
  91. return operations
  92. class DocGenerator(object):
  93. def get_doc(
  94. self,
  95. controllers: typing.List[DecoratedController],
  96. context: ContextInterface,
  97. title: str='',
  98. description: str='',
  99. ) -> dict:
  100. """
  101. Generate an OpenApi 2.0 documentation. Th given context will be used
  102. to found controllers matching with given DecoratedController.
  103. :param controllers: List of DecoratedController to match with context
  104. controllers
  105. :param context: a context instance
  106. :param title: The generated doc title
  107. :param description: The generated doc description
  108. :return: a apispec documentation dict
  109. """
  110. spec = APISpec(
  111. title=title,
  112. info=dict(description=description),
  113. version='1.0.0',
  114. plugins=(
  115. 'apispec.ext.marshmallow',
  116. ),
  117. schema_name_resolver=generate_schema_name,
  118. )
  119. schemas = []
  120. # parse schemas
  121. for controller in controllers:
  122. description = controller.description
  123. if description.input_body:
  124. schemas.append(type(
  125. description.input_body.wrapper.processor.schema
  126. ))
  127. if description.input_forms:
  128. schemas.append(type(
  129. description.input_forms.wrapper.processor.schema
  130. ))
  131. if description.output_body:
  132. schemas.append(type(
  133. description.output_body.wrapper.processor.schema
  134. ))
  135. if description.errors:
  136. for error in description.errors:
  137. schemas.append(type(error.wrapper.error_builder))
  138. for schema in set(schemas):
  139. spec.definition(schema.__name__, schema=schema)
  140. # add views
  141. # with app.test_request_context():
  142. paths = {}
  143. for controller in controllers:
  144. route = context.find_route(controller)
  145. swagger_path = context.get_swagger_path(route.rule)
  146. operations = bottle_generate_operations(
  147. spec,
  148. route,
  149. controller.description,
  150. )
  151. # TODO BS 20171114: TMP code waiting refact of doc
  152. doc_string = controller.reference.get_doc_string()
  153. if doc_string:
  154. for method in operations.keys():
  155. operations[method]['description'] = doc_string
  156. path = Path(path=swagger_path, operations=operations)
  157. if swagger_path in paths:
  158. paths[swagger_path].update(path)
  159. else:
  160. paths[swagger_path] = path
  161. spec.add_path(path)
  162. return spec.to_dict()
  163. def get_doc_yaml(
  164. self,
  165. controllers: typing.List[DecoratedController],
  166. context: ContextInterface,
  167. title: str = '',
  168. description: str = '',
  169. ) -> str:
  170. dict_doc = self.get_doc(
  171. controllers=controllers,
  172. context=context,
  173. title=title,
  174. description=description,
  175. )
  176. json_doc = json.dumps(dict_doc)
  177. # We dump then load with json to use real scalar dict.
  178. # If not, yaml dump dict-like objects
  179. clean_dict_doc = json.loads(json_doc)
  180. return yaml.dump(clean_dict_doc, default_flow_style=False)
  181. def save_in_file(
  182. self,
  183. doc_file_path: str,
  184. controllers: typing.List[DecoratedController],
  185. context: ContextInterface,
  186. title: str='',
  187. description: str='',
  188. ) -> None:
  189. doc_yaml = self.get_doc_yaml(
  190. controllers=controllers,
  191. context=context,
  192. title=title,
  193. description=description,
  194. )
  195. with open(doc_file_path, 'w+') as doc_file:
  196. doc_file.write(doc_yaml)
  197. # TODO BS 20171109: Must take care of already existing definition names
  198. def generate_schema_name(schema):
  199. return schema.__name__