schema_resolver.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. from .common import resolve_schema_instance
  2. class SchemaResolver:
  3. """Resolve marshmallow Schemas in OpenAPI components and translate to OpenAPI
  4. `schema objects
  5. <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#schema-object>`_,
  6. `parameter objects
  7. <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object>`_
  8. or `reference objects
  9. <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#reference-object>`_.
  10. """
  11. def __init__(self, openapi_version, converter):
  12. self.openapi_version = openapi_version
  13. self.converter = converter
  14. def resolve_operations(self, operations, **kwargs):
  15. """Resolve marshmallow Schemas in a dict mapping operation to OpenApi `Operation Object
  16. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#operationObject`_
  17. """
  18. for operation in operations.values():
  19. if not isinstance(operation, dict):
  20. continue
  21. if "parameters" in operation:
  22. operation["parameters"] = self.resolve_parameters(
  23. operation["parameters"]
  24. )
  25. if self.openapi_version.major >= 3:
  26. self.resolve_callback(operation.get("callbacks", {}))
  27. if "requestBody" in operation:
  28. self.resolve_schema(operation["requestBody"])
  29. for response in operation.get("responses", {}).values():
  30. self.resolve_response(response)
  31. def resolve_callback(self, callbacks):
  32. """Resolve marshmallow Schemas in a dict mapping callback name to OpenApi `Callback Object
  33. https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#callbackObject`_.
  34. This is done recursively, so it is possible to define callbacks in your callbacks.
  35. Example: ::
  36. # Input
  37. {
  38. "userEvent": {
  39. "https://my.example/user-callback": {
  40. "post": {
  41. "requestBody": {
  42. "content": {"application/json": {"schema": UserSchema}}
  43. }
  44. },
  45. }
  46. }
  47. }
  48. # Output
  49. {
  50. "userEvent": {
  51. "https://my.example/user-callback": {
  52. "post": {
  53. "requestBody": {
  54. "content": {
  55. "application/json": {
  56. "schema": {"$ref": "#/components/schemas/User"}
  57. }
  58. }
  59. }
  60. },
  61. }
  62. }
  63. }
  64. """
  65. for callback in callbacks.values():
  66. if isinstance(callback, dict):
  67. for path in callback.values():
  68. self.resolve_operations(path)
  69. def resolve_parameters(self, parameters):
  70. """Resolve marshmallow Schemas in a list of OpenAPI `Parameter Objects
  71. <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#parameter-object>`_.
  72. Each parameter object that contains a Schema will be translated into
  73. one or more Parameter Objects.
  74. If the value of a `schema` key is marshmallow Schema class, instance or
  75. a string that resolves to a Schema Class each field in the Schema will
  76. be expanded as a separate Parameter Object.
  77. Example: ::
  78. # Input
  79. class UserSchema(Schema):
  80. name = fields.String()
  81. id = fields.Int()
  82. [{"in": "query", "schema": "UserSchema"}]
  83. # Output
  84. [
  85. {
  86. "in": "query",
  87. "name": "id",
  88. "required": False,
  89. "schema": {"type": "integer"},
  90. },
  91. {
  92. "in": "query",
  93. "name": "name",
  94. "required": False,
  95. "schema": {"type": "string"},
  96. },
  97. ]
  98. If the Parameter Object contains a `content` key a single Parameter
  99. Object is returned with the Schema translated into a Schema Object or
  100. Reference Object.
  101. Example: ::
  102. # Input
  103. [
  104. {
  105. "in": "query",
  106. "name": "pet",
  107. "content": {"application/json": {"schema": "PetSchema"}},
  108. }
  109. ]
  110. # Output
  111. [
  112. {
  113. "in": "query",
  114. "name": "pet",
  115. "content": {
  116. "application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}
  117. },
  118. }
  119. ]
  120. :param list parameters: the list of OpenAPI parameter objects to resolve.
  121. """
  122. resolved = []
  123. for parameter in parameters:
  124. if (
  125. isinstance(parameter, dict)
  126. and not isinstance(parameter.get("schema", {}), dict)
  127. and "in" in parameter
  128. ):
  129. schema_instance = resolve_schema_instance(parameter.pop("schema"))
  130. resolved += self.converter.schema2parameters(
  131. schema_instance, location=parameter.pop("in"), **parameter
  132. )
  133. else:
  134. self.resolve_schema(parameter)
  135. resolved.append(parameter)
  136. return resolved
  137. def resolve_response(self, response):
  138. """Resolve marshmallow Schemas in OpenAPI `Response Objects
  139. <https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#responseObject>`_.
  140. Schemas may appear in either a Media Type Object or a Header Object.
  141. Example: ::
  142. # Input
  143. {
  144. "content": {"application/json": {"schema": "PetSchema"}},
  145. "description": "successful operation",
  146. "headers": {"PetHeader": {"schema": "PetHeaderSchema"}},
  147. }
  148. # Output
  149. {
  150. "content": {
  151. "application/json": {"schema": {"$ref": "#/components/schemas/Pet"}}
  152. },
  153. "description": "successful operation",
  154. "headers": {
  155. "PetHeader": {"schema": {"$ref": "#/components/schemas/PetHeader"}}
  156. },
  157. }
  158. :param dict response: the response object to resolve.
  159. """
  160. self.resolve_schema(response)
  161. if "headers" in response:
  162. for header in response["headers"].values():
  163. self.resolve_schema(header)
  164. def resolve_schema(self, data):
  165. """Resolve marshmallow Schemas in an OpenAPI component or header -
  166. modifies the input dictionary to translate marshmallow Schemas to OpenAPI
  167. Schema Objects or Reference Objects.
  168. OpenAPIv3 Components: ::
  169. # Input
  170. {
  171. "description": "user to add to the system",
  172. "content": {"application/json": {"schema": "UserSchema"}},
  173. }
  174. # Output
  175. {
  176. "description": "user to add to the system",
  177. "content": {
  178. "application/json": {"schema": {"$ref": "#/components/schemas/User"}}
  179. },
  180. }
  181. :param dict|str data: either a parameter or response dictionary that may
  182. contain a schema, or a reference provided as string
  183. """
  184. if not isinstance(data, dict):
  185. return
  186. # OAS 2 component or OAS 3 parameter or header
  187. if "schema" in data:
  188. data["schema"] = self.resolve_schema_dict(data["schema"])
  189. # OAS 3 component except header
  190. if self.openapi_version.major >= 3:
  191. if "content" in data:
  192. for content in data["content"].values():
  193. if "schema" in content:
  194. content["schema"] = self.resolve_schema_dict(content["schema"])
  195. def resolve_schema_dict(self, schema):
  196. """Resolve a marshmallow Schema class, object, or a string that resolves
  197. to a Schema class or a schema reference or an OpenAPI Schema Object
  198. containing one of the above to an OpenAPI Schema Object or Reference Object.
  199. If the input is a marshmallow Schema class, object or a string that resolves
  200. to a Schema class the Schema will be translated to an OpenAPI Schema Object
  201. or Reference Object.
  202. Example: ::
  203. # Input
  204. "PetSchema"
  205. # Output
  206. {"$ref": "#/components/schemas/Pet"}
  207. If the input is a dictionary representation of an OpenAPI Schema Object
  208. recursively search for a marshmallow Schemas to resolve. For `"type": "array"`,
  209. marshmallow Schemas may appear as the value of the `items` key. For
  210. `"type": "object"` Marshmalow Schemas may appear as values in the `properties`
  211. dictionary.
  212. Examples: ::
  213. # Input
  214. {"type": "array", "items": "PetSchema"}
  215. # Output
  216. {"type": "array", "items": {"$ref": "#/components/schemas/Pet"}}
  217. # Input
  218. {"type": "object", "properties": {"pet": "PetSchcema", "user": "UserSchema"}}
  219. # Output
  220. {
  221. "type": "object",
  222. "properties": {
  223. "pet": {"$ref": "#/components/schemas/Pet"},
  224. "user": {"$ref": "#/components/schemas/User"},
  225. },
  226. }
  227. :param string|Schema|dict schema: the schema to resolve.
  228. """
  229. if isinstance(schema, dict):
  230. if schema.get("type") == "array" and "items" in schema:
  231. schema["items"] = self.resolve_schema_dict(schema["items"])
  232. if schema.get("type") == "object" and "properties" in schema:
  233. schema["properties"] = {
  234. k: self.resolve_schema_dict(v)
  235. for k, v in schema["properties"].items()
  236. }
  237. for keyword in ("oneOf", "anyOf", "allOf"):
  238. if keyword in schema:
  239. schema[keyword] = [
  240. self.resolve_schema_dict(s) for s in schema[keyword]
  241. ]
  242. if "not" in schema:
  243. schema["not"] = self.resolve_schema_dict(schema["not"])
  244. return schema
  245. return self.converter.resolve_nested_schema(schema)