doc.py 9.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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) -> str:
  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. schema_name_resolver=generate_schema_name
  143. )
  144. schemas = []
  145. # parse schemas
  146. for controller in controllers:
  147. description = controller.description
  148. if description.input_body:
  149. schemas.append(spec.schema_class_resolver(
  150. spec,
  151. description.input_body.wrapper.processor.schema
  152. ))
  153. if description.input_forms:
  154. schemas.append(spec.schema_class_resolver(
  155. spec,
  156. description.input_forms.wrapper.processor.schema
  157. ))
  158. if description.output_body:
  159. schemas.append(spec.schema_class_resolver(
  160. spec,
  161. description.output_body.wrapper.processor.schema
  162. ))
  163. if description.errors:
  164. for error in description.errors:
  165. schemas.append(type(error.wrapper.error_builder))
  166. for schema in set(schemas):
  167. spec.definition(
  168. spec.schema_name_resolver(schema),
  169. schema=schema
  170. )
  171. # add views
  172. # with app.test_request_context():
  173. paths = {}
  174. for controller in controllers:
  175. route = context.find_route(controller)
  176. swagger_path = context.get_swagger_path(route.rule)
  177. operations = bottle_generate_operations(
  178. spec,
  179. route,
  180. controller.description,
  181. )
  182. # TODO BS 20171114: TMP code waiting refact of doc
  183. doc_string = controller.reference.get_doc_string()
  184. if doc_string:
  185. for method in operations.keys():
  186. operations[method]['description'] = doc_string
  187. path = Path(path=swagger_path, operations=operations)
  188. if swagger_path in paths:
  189. paths[swagger_path].update(path)
  190. else:
  191. paths[swagger_path] = path
  192. spec.add_path(path)
  193. return spec.to_dict()
  194. def get_doc_yaml(
  195. self,
  196. controllers: typing.List[DecoratedController],
  197. context: ContextInterface,
  198. title: str = '',
  199. description: str = '',
  200. ) -> str:
  201. dict_doc = self.get_doc(
  202. controllers=controllers,
  203. context=context,
  204. title=title,
  205. description=description,
  206. )
  207. json_doc = json.dumps(dict_doc)
  208. # We dump then load with json to use real scalar dict.
  209. # If not, yaml dump dict-like objects
  210. clean_dict_doc = json.loads(json_doc)
  211. return yaml.dump(clean_dict_doc, default_flow_style=False)
  212. def save_in_file(
  213. self,
  214. doc_file_path: str,
  215. controllers: typing.List[DecoratedController],
  216. context: ContextInterface,
  217. title: str='',
  218. description: str='',
  219. ) -> None:
  220. doc_yaml = self.get_doc_yaml(
  221. controllers=controllers,
  222. context=context,
  223. title=title,
  224. description=description,
  225. )
  226. with open(doc_file_path, 'w+') as doc_file:
  227. doc_file.write(doc_yaml)
  228. # TODO BS 20171109: Must take care of already existing definition names
  229. def generate_schema_name(schema: marshmallow.Schema):
  230. """
  231. Return best candidate name for one schema cls or instance.
  232. :param schema: instance or cls schema
  233. :return: best schema name
  234. """
  235. if not isinstance(schema, type):
  236. schema = type(schema)
  237. if getattr(schema, '_schema_name', None):
  238. if schema.opts.exclude:
  239. schema_name = "{}_without".format(schema.__name__)
  240. for elem in sorted(schema.opts.exclude):
  241. schema_name="{}_{}".format(schema_name, elem)
  242. else:
  243. schema_name = schema._schema_name
  244. else:
  245. schema_name = schema.__name__
  246. return schema_name