1"""
2This module defines a Swagger2Operation class, a Connexion operation specific for Swagger 2 specs.
3"""
4
5import logging
6import typing as t
7from http import HTTPStatus
8
9from connexion.datastructures import NoContent
10from connexion.exceptions import InvalidSpecification
11from connexion.operations.abstract import AbstractOperation
12from connexion.uri_parsing import Swagger2URIParser
13from connexion.utils import build_example_from_schema, deep_get
14
15logger = logging.getLogger("connexion.operations.swagger2")
16
17
18COLLECTION_FORMAT_MAPPING = {
19 "multi": {"style": "form", "explode": True},
20 "csv": {"style": "form", "explode": False},
21 "ssv": {"style": "spaceDelimited", "explode": False},
22 "pipes": {"style": "pipeDelimited", "explode": False},
23}
24
25
26class Swagger2Operation(AbstractOperation):
27 """
28 Exposes a Swagger 2.0 operation under the AbstractOperation interface.
29 The primary purpose of this class is to provide the `function()` method
30 to the API. A Swagger2Operation is plugged into the API with the provided
31 (path, method) pair. It resolves the handler function for this operation
32 with the provided resolver, and wraps the handler function with multiple
33 decorators that provide security, validation, serialization,
34 and deserialization.
35 """
36
37 def __init__(
38 self,
39 method,
40 path,
41 operation,
42 resolver,
43 app_produces,
44 app_consumes,
45 path_parameters=None,
46 app_security=None,
47 security_schemes=None,
48 definitions=None,
49 randomize_endpoint=None,
50 uri_parser_class=None,
51 ):
52 """
53 :param method: HTTP method
54 :type method: str
55 :param path: relative path to this operation
56 :type path: str
57 :param operation: swagger operation object
58 :type operation: dict
59 :param resolver: Callable that maps operationID to a function
60 :type resolver: resolver.Resolver
61 :param app_produces: list of content types the application can return by default
62 :type app_produces: list
63 :param app_consumes: list of content types the application consumes by default
64 :type app_consumes: list
65 :param path_parameters: Parameters defined in the path level
66 :type path_parameters: list
67 :param app_security: list of security rules the application uses by default
68 :type app_security: list
69 :param security_schemes: `Security Definitions Object
70 <https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#security-definitions-object>`_
71 :type security_schemes: dict
72 :param definitions: `Definitions Object
73 <https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#definitionsObject>`_
74 :type definitions: dict
75 :param randomize_endpoint: number of random characters to append to operation name
76 :type randomize_endpoint: integer
77 :param uri_parser_class: class to use for uri parsing
78 :type uri_parser_class: AbstractURIParser
79 """
80 uri_parser_class = uri_parser_class or Swagger2URIParser
81
82 self._router_controller = operation.get("x-swagger-router-controller")
83
84 super().__init__(
85 method=method,
86 path=path,
87 operation=operation,
88 resolver=resolver,
89 app_security=app_security,
90 security_schemes=security_schemes,
91 randomize_endpoint=randomize_endpoint,
92 uri_parser_class=uri_parser_class,
93 )
94
95 self._produces = operation.get("produces", app_produces)
96 self._consumes = operation.get("consumes", app_consumes)
97
98 self.definitions = definitions or {}
99
100 self._parameters = operation.get("parameters", [])
101 if path_parameters:
102 self._parameters += path_parameters
103
104 self._responses = operation.get("responses", {})
105
106 @classmethod
107 def from_spec(cls, spec, *args, path, method, resolver, **kwargs):
108 return cls(
109 method,
110 path,
111 spec.get_operation(path, method),
112 resolver=resolver,
113 path_parameters=spec.get_path_params(path),
114 app_produces=spec.produces,
115 app_consumes=spec.consumes,
116 app_security=spec.security,
117 security_schemes=spec.security_schemes,
118 definitions=spec.definitions,
119 *args,
120 **kwargs,
121 )
122
123 @property
124 def request_body(self) -> dict:
125 if not hasattr(self, "_request_body"):
126 body_params = []
127 form_params = []
128 for parameter in self.parameters:
129 if parameter["in"] == "body":
130 body_params.append(parameter)
131 elif parameter["in"] == "formData":
132 form_params.append(parameter)
133
134 if len(body_params) > 1:
135 raise InvalidSpecification(
136 f"{self.method} {self.path}: There can be one 'body' parameter at most"
137 )
138
139 if body_params and form_params:
140 raise InvalidSpecification(
141 f"{self.method} {self.path}: 'body' and 'formData' parameters are mutually exclusive"
142 )
143
144 if body_params:
145 self._request_body = self._transform_json(body_params[0])
146 elif form_params:
147 self._request_body = self._transform_form(form_params)
148 else:
149 self._request_body = {}
150
151 return self._request_body
152
153 @property
154 def parameters(self):
155 return self._parameters
156
157 @property
158 def consumes(self):
159 return self._consumes
160
161 @property
162 def produces(self):
163 return self._produces
164
165 def get_path_parameter_types(self):
166 types = {}
167 path_parameters = (p for p in self.parameters if p["in"] == "path")
168 for path_defn in path_parameters:
169 if path_defn.get("type") == "string" and path_defn.get("format") == "path":
170 # path is special case for type 'string'
171 path_type = "path"
172 else:
173 path_type = path_defn.get("type")
174 types[path_defn["name"]] = path_type
175 return types
176
177 def with_definitions(self, schema):
178 if "schema" in schema:
179 schema["schema"]["definitions"] = self.definitions
180 return schema
181
182 def response_schema(self, status_code=None, content_type=None):
183 response_definition = self.response_definition(status_code, content_type)
184 return self.with_definitions(response_definition.get("schema", {}))
185
186 def example_response(self, status_code=None, *args, **kwargs):
187 """
188 Returns example response from spec
189 """
190 # simply use the first/lowest status code, this is probably 200 or 201
191 status_code = status_code or sorted(self._responses.keys())[0]
192 examples_path = [str(status_code), "examples"]
193 schema_example_path = [str(status_code), "schema", "example"]
194 schema_path = [str(status_code), "schema"]
195
196 try:
197 status_code = int(status_code)
198 except ValueError:
199 status_code = 200
200
201 if status_code == HTTPStatus.NO_CONTENT:
202 return NoContent, status_code
203
204 try:
205 return (
206 list(deep_get(self._responses, examples_path).values())[0],
207 status_code,
208 )
209 except KeyError:
210 pass
211 try:
212 return (deep_get(self._responses, schema_example_path), status_code)
213 except KeyError:
214 pass
215
216 try:
217 schema = deep_get(self._responses, schema_path)
218 except KeyError:
219 return ("No example response or response schema defined.", status_code)
220
221 return (build_example_from_schema(schema), status_code)
222
223 def body_name(self, content_type: t.Optional[str] = None) -> str:
224 return self.body_definition(content_type).get("name", "body")
225
226 def body_schema(self, content_type: t.Optional[str] = None) -> dict:
227 """
228 The body schema definition for this operation.
229 """
230 body_definition = self.body_definition(content_type)
231 return self.with_definitions(body_definition).get("schema", {})
232
233 def body_definition(self, content_type: t.Optional[str] = None) -> dict:
234 """
235 The body complete definition for this operation.
236
237 **There can be one "body" parameter at most.**
238 """
239 return self.request_body
240
241 def _transform_json(self, body_parameter: dict) -> dict:
242 """Translate Swagger2 json parameters into OpenAPI 3 jsonschema spec."""
243 nullable = body_parameter.get("x-nullable")
244 if nullable is not None:
245 body_parameter["schema"]["nullable"] = nullable
246 return body_parameter
247
248 def _transform_form(self, form_parameters: t.List[dict]) -> dict:
249 """Translate Swagger2 form parameters into OpenAPI 3 jsonschema spec."""
250 properties = {}
251 defaults = {}
252 required = []
253 encoding = {}
254
255 for param in form_parameters:
256 prop = {}
257
258 if param["type"] == "file":
259 prop.update(
260 {
261 "type": "string",
262 "format": "binary",
263 }
264 )
265 else:
266 prop["type"] = param["type"]
267
268 format_ = param.get("format")
269 if format_ is not None:
270 prop["format"] = format_
271
272 default = param.get("default")
273 if default is not None:
274 prop["default"] = default
275 defaults[param["name"]] = default
276
277 nullable = param.get("x-nullable")
278 if nullable is not None:
279 prop["nullable"] = nullable
280
281 if param["type"] == "array":
282 prop["items"] = param.get("items", {})
283
284 collection_format = param.get("collectionFormat", "csv")
285 try:
286 encoding[param["name"]] = COLLECTION_FORMAT_MAPPING[
287 collection_format
288 ]
289 except KeyError:
290 raise InvalidSpecification(
291 f"The collection format ({collection_format}) is not supported by "
292 f"Connexion as it cannot be mapped to OpenAPI 3."
293 )
294
295 properties[param["name"]] = prop
296
297 if param.get("required", False):
298 required.append(param["name"])
299
300 definition: t.Dict[str, t.Any] = {
301 "schema": {
302 "type": "object",
303 "properties": properties,
304 "required": required,
305 }
306 }
307 if defaults:
308 definition["schema"]["default"] = defaults
309 if encoding:
310 definition["encoding"] = encoding
311
312 return definition