doc.py 8.4KB

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