doc.py 8.4KB

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