doc.py 8.2KB

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