doc.py 8.4KB

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