123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- # -*- coding: utf-8 -*-
- import json
-
- import typing
- import yaml
-
- from apispec import APISpec
- from apispec import Path
- from apispec.ext.marshmallow.swagger import schema2jsonschema
-
- from hapic.context import ContextInterface
- from hapic.context import RouteRepresentation
- from hapic.decorator import DecoratedController
- from hapic.description import ControllerDescription
-
-
- def generate_schema_ref(spec, schema):
- schema_class = spec.schema_class_resolver(
- spec,
- schema
- )
- ref = {
- '$ref': '#/definitions/{}'.format(
- spec.schema_name_resolver(schema_class)
- )
- }
- if schema.many:
- ref = {
- 'type': 'array',
- 'items': ref
- }
- return ref
-
-
- def bottle_generate_operations(
- spec,
- route: RouteRepresentation,
- description: ControllerDescription,
- ):
- method_operations = dict()
- if description.input_body:
- method_operations.setdefault('parameters', []).append({
- 'in': 'body',
- 'name': 'body',
- 'schema': generate_schema_ref(
- spec,
- description.input_body.wrapper.processor.schema,
- )
- })
-
- if description.output_body:
- method_operations.setdefault('responses', {})\
- [int(description.output_body.wrapper.default_http_code)] = {
- 'description': str(int(description.output_body.wrapper.default_http_code)), # nopep8
- 'schema': generate_schema_ref(
- spec,
- description.output_body.wrapper.processor.schema,
- )
- }
-
- if description.output_file:
- method_operations.setdefault('produces', []).extend(
- description.output_file.wrapper.output_types
- )
- method_operations.setdefault('responses', {})\
- [int(description.output_file.wrapper.default_http_code)] = {
- 'description': str(int(description.output_file.wrapper.default_http_code)), # nopep8
- }
-
- if description.errors:
- for error in description.errors:
- schema_class = type(error.wrapper.error_builder)
- method_operations.setdefault('responses', {})\
- [int(error.wrapper.http_code)] = {
- 'description': str(int(error.wrapper.http_code)),
- 'schema': {
- '$ref': '#/definitions/{}'.format(
- spec.schema_name_resolver(schema_class)
- )
- }
- }
-
- # jsonschema based
- if description.input_path:
- schema_class = spec.schema_class_resolver(
- spec,
- description.input_path.wrapper.processor.schema
- )
- # TODO: look schema2parameters ?
- jsonschema = schema2jsonschema(schema_class, spec=spec)
- for name, schema in jsonschema.get('properties', {}).items():
- method_operations.setdefault('parameters', []).append({
- 'in': 'path',
- 'name': name,
- 'required': name in jsonschema.get('required', []),
- 'type': schema['type']
- })
-
- if description.input_query:
- schema_class = spec.schema_class_resolver(
- spec,
- description.input_query.wrapper.processor.schema
- )
- jsonschema = schema2jsonschema(schema_class, spec=spec)
- for name, schema in jsonschema.get('properties', {}).items():
- method_operations.setdefault('parameters', []).append({
- 'in': 'query',
- 'name': name,
- 'required': name in jsonschema.get('required', []),
- 'type': schema['type']
- })
-
- if description.input_files:
- method_operations.setdefault('consumes', []).append('multipart/form-data')
- for field_name, field in description.input_files.wrapper.processor.schema.fields.items():
- method_operations.setdefault('parameters', []).append({
- 'in': 'formData',
- 'name': field_name,
- 'required': field.required,
- 'type': 'file',
- })
-
- if description.tags:
- method_operations['tags'] = description.tags
-
- operations = {
- route.method.lower(): method_operations,
- }
-
- return operations
-
-
- class DocGenerator(object):
- def get_doc(
- self,
- controllers: typing.List[DecoratedController],
- context: ContextInterface,
- title: str='',
- description: str='',
- ) -> dict:
- """
- Generate an OpenApi 2.0 documentation. Th given context will be used
- to found controllers matching with given DecoratedController.
- :param controllers: List of DecoratedController to match with context
- controllers
- :param context: a context instance
- :param title: The generated doc title
- :param description: The generated doc description
- :return: a apispec documentation dict
- """
- spec = APISpec(
- title=title,
- info=dict(description=description),
- version='1.0.0',
- plugins=(
- 'apispec.ext.marshmallow',
- ),
- auto_referencing=True,
- )
-
- schemas = []
- # parse schemas
- for controller in controllers:
- description = controller.description
-
- if description.input_body:
- schemas.append(spec.schema_class_resolver(
- spec,
- description.input_body.wrapper.processor.schema
- ))
-
- if description.input_forms:
- schemas.append(spec.schema_class_resolver(
- spec,
- description.input_forms.wrapper.processor.schema
- ))
-
- if description.output_body:
- schemas.append(spec.schema_class_resolver(
- spec,
- description.output_body.wrapper.processor.schema
- ))
-
- if description.errors:
- for error in description.errors:
- schemas.append(type(error.wrapper.error_builder))
-
- for schema in set(schemas):
- spec.definition(
- spec.schema_name_resolver(schema),
- schema=schema
- )
-
- # add views
- # with app.test_request_context():
- paths = {}
- for controller in controllers:
- route = context.find_route(controller)
- swagger_path = context.get_swagger_path(route.rule)
-
- operations = bottle_generate_operations(
- spec,
- route,
- controller.description,
- )
-
- # TODO BS 20171114: TMP code waiting refact of doc
- doc_string = controller.reference.get_doc_string()
- if doc_string:
- for method in operations.keys():
- operations[method]['description'] = doc_string
-
- path = Path(path=swagger_path, operations=operations)
-
- if swagger_path in paths:
- paths[swagger_path].update(path)
- else:
- paths[swagger_path] = path
-
- spec.add_path(path)
-
- return spec.to_dict()
-
- def get_doc_yaml(
- self,
- controllers: typing.List[DecoratedController],
- context: ContextInterface,
- title: str = '',
- description: str = '',
- ) -> str:
- dict_doc = self.get_doc(
- controllers=controllers,
- context=context,
- title=title,
- description=description,
- )
- json_doc = json.dumps(dict_doc)
-
- # We dump then load with json to use real scalar dict.
- # If not, yaml dump dict-like objects
- clean_dict_doc = json.loads(json_doc)
- return yaml.dump(clean_dict_doc, default_flow_style=False)
-
- def save_in_file(
- self,
- doc_file_path: str,
- controllers: typing.List[DecoratedController],
- context: ContextInterface,
- title: str='',
- description: str='',
- ) -> None:
- doc_yaml = self.get_doc_yaml(
- controllers=controllers,
- context=context,
- title=title,
- description=description,
- )
- with open(doc_file_path, 'w+') as doc_file:
- doc_file.write(doc_yaml)
-
-
- # TODO BS 20171109: Must take care of already existing definition names
- def generate_schema_name(schema):
- return schema.__name__
|