1# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"). You
4# may not use this file except in compliance with the License. A copy of
5# the License is located at
6#
7# http://aws.amazon.com/apache2.0/
8#
9# or in the "license" file accompanying this file. This file is
10# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11# ANY KIND, either express or implied. See the License for the specific
12# language governing permissions and limitations under the License.
13"""Protocol input serializes.
14
15This module contains classes that implement input serialization
16for the various AWS protocol types.
17
18These classes essentially take user input, a model object that
19represents what the expected input should look like, and it returns
20a dictionary that contains the various parts of a request. A few
21high level design decisions:
22
23
24* Each protocol type maps to a separate class, all inherit from
25 ``Serializer``.
26* The return value for ``serialize_to_request`` (the main entry
27 point) returns a dictionary that represents a request. This
28 will have keys like ``url_path``, ``query_string``, etc. This
29 is done so that it's a) easy to test and b) not tied to a
30 particular HTTP library. See the ``serialize_to_request`` docstring
31 for more details.
32
33Unicode
34-------
35
36The input to the serializers should be text (str/unicode), not bytes,
37with the exception of blob types. Those are assumed to be binary,
38and if a str/unicode type is passed in, it will be encoded as utf-8.
39"""
40
41import base64
42import calendar
43import datetime
44import json
45import re
46from xml.etree import ElementTree
47
48from botocore import validate
49from botocore.compat import formatdate
50from botocore.exceptions import ParamValidationError
51from botocore.utils import (
52 has_header,
53 is_json_value_header,
54 parse_to_aware_datetime,
55 percent_encode,
56)
57
58# From the spec, the default timestamp format if not specified is iso8601.
59DEFAULT_TIMESTAMP_FORMAT = 'iso8601'
60ISO8601 = '%Y-%m-%dT%H:%M:%SZ'
61# Same as ISO8601, but with microsecond precision.
62ISO8601_MICRO = '%Y-%m-%dT%H:%M:%S.%fZ'
63HOST_PREFIX_RE = re.compile(r"^[A-Za-z0-9\.\-]+$")
64
65
66def create_serializer(protocol_name, include_validation=True):
67 # TODO: Unknown protocols.
68 serializer = SERIALIZERS[protocol_name]()
69 if include_validation:
70 validator = validate.ParamValidator()
71 serializer = validate.ParamValidationDecorator(validator, serializer)
72 return serializer
73
74
75class Serializer:
76 DEFAULT_METHOD = 'POST'
77 # Clients can change this to a different MutableMapping
78 # (i.e OrderedDict) if they want. This is used in the
79 # compliance test to match the hash ordering used in the
80 # tests.
81 MAP_TYPE = dict
82 DEFAULT_ENCODING = 'utf-8'
83
84 def serialize_to_request(self, parameters, operation_model):
85 """Serialize parameters into an HTTP request.
86
87 This method takes user provided parameters and a shape
88 model and serializes the parameters to an HTTP request.
89 More specifically, this method returns information about
90 parts of the HTTP request, it does not enforce a particular
91 interface or standard for an HTTP request. It instead returns
92 a dictionary of:
93
94 * 'url_path'
95 * 'host_prefix'
96 * 'query_string'
97 * 'headers'
98 * 'body'
99 * 'method'
100
101 It is then up to consumers to decide how to map this to a Request
102 object of their HTTP library of choice. Below is an example
103 return value::
104
105 {'body': {'Action': 'OperationName',
106 'Bar': 'val2',
107 'Foo': 'val1',
108 'Version': '2014-01-01'},
109 'headers': {},
110 'method': 'POST',
111 'query_string': '',
112 'host_prefix': 'value.',
113 'url_path': '/'}
114
115 :param parameters: The dictionary input parameters for the
116 operation (i.e the user input).
117 :param operation_model: The OperationModel object that describes
118 the operation.
119 """
120 raise NotImplementedError("serialize_to_request")
121
122 def _create_default_request(self):
123 # Creates a boilerplate default request dict that subclasses
124 # can use as a starting point.
125 serialized = {
126 'url_path': '/',
127 'query_string': '',
128 'method': self.DEFAULT_METHOD,
129 'headers': {},
130 # An empty body is represented as an empty byte string.
131 'body': b'',
132 }
133 return serialized
134
135 # Some extra utility methods subclasses can use.
136
137 def _timestamp_iso8601(self, value):
138 if value.microsecond > 0:
139 timestamp_format = ISO8601_MICRO
140 else:
141 timestamp_format = ISO8601
142 return value.strftime(timestamp_format)
143
144 def _timestamp_unixtimestamp(self, value):
145 return int(calendar.timegm(value.timetuple()))
146
147 def _timestamp_rfc822(self, value):
148 if isinstance(value, datetime.datetime):
149 value = self._timestamp_unixtimestamp(value)
150 return formatdate(value, usegmt=True)
151
152 def _convert_timestamp_to_str(self, value, timestamp_format=None):
153 if timestamp_format is None:
154 timestamp_format = self.TIMESTAMP_FORMAT
155 timestamp_format = timestamp_format.lower()
156 datetime_obj = parse_to_aware_datetime(value)
157 converter = getattr(self, f'_timestamp_{timestamp_format}')
158 final_value = converter(datetime_obj)
159 return final_value
160
161 def _get_serialized_name(self, shape, default_name):
162 # Returns the serialized name for the shape if it exists.
163 # Otherwise it will return the passed in default_name.
164 return shape.serialization.get('name', default_name)
165
166 def _get_base64(self, value):
167 # Returns the base64-encoded version of value, handling
168 # both strings and bytes. The returned value is a string
169 # via the default encoding.
170 if isinstance(value, str):
171 value = value.encode(self.DEFAULT_ENCODING)
172 return base64.b64encode(value).strip().decode(self.DEFAULT_ENCODING)
173
174 def _expand_host_prefix(self, parameters, operation_model):
175 operation_endpoint = operation_model.endpoint
176 if (
177 operation_endpoint is None
178 or 'hostPrefix' not in operation_endpoint
179 ):
180 return None
181
182 host_prefix_expression = operation_endpoint['hostPrefix']
183 input_members = operation_model.input_shape.members
184 host_labels = [
185 member
186 for member, shape in input_members.items()
187 if shape.serialization.get('hostLabel')
188 ]
189 format_kwargs = {}
190 bad_labels = []
191 for name in host_labels:
192 param = parameters[name]
193 if not HOST_PREFIX_RE.match(param):
194 bad_labels.append(name)
195 format_kwargs[name] = param
196 if bad_labels:
197 raise ParamValidationError(
198 report=(
199 f"Invalid value for parameter(s): {', '.join(bad_labels)}. "
200 "Must contain only alphanumeric characters, hyphen, "
201 "or period."
202 )
203 )
204 return host_prefix_expression.format(**format_kwargs)
205
206
207class QuerySerializer(Serializer):
208 TIMESTAMP_FORMAT = 'iso8601'
209
210 def serialize_to_request(self, parameters, operation_model):
211 shape = operation_model.input_shape
212 serialized = self._create_default_request()
213 serialized['method'] = operation_model.http.get(
214 'method', self.DEFAULT_METHOD
215 )
216 serialized['headers'] = {
217 'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'
218 }
219 # The query serializer only deals with body params so
220 # that's what we hand off the _serialize_* methods.
221 body_params = self.MAP_TYPE()
222 body_params['Action'] = operation_model.name
223 body_params['Version'] = operation_model.metadata['apiVersion']
224 if shape is not None:
225 self._serialize(body_params, parameters, shape)
226 serialized['body'] = body_params
227
228 host_prefix = self._expand_host_prefix(parameters, operation_model)
229 if host_prefix is not None:
230 serialized['host_prefix'] = host_prefix
231
232 return serialized
233
234 def _serialize(self, serialized, value, shape, prefix=''):
235 # serialized: The dict that is incrementally added to with the
236 # final serialized parameters.
237 # value: The current user input value.
238 # shape: The shape object that describes the structure of the
239 # input.
240 # prefix: The incrementally built up prefix for the serialized
241 # key (i.e Foo.bar.members.1).
242 method = getattr(
243 self,
244 f'_serialize_type_{shape.type_name}',
245 self._default_serialize,
246 )
247 method(serialized, value, shape, prefix=prefix)
248
249 def _serialize_type_structure(self, serialized, value, shape, prefix=''):
250 members = shape.members
251 for key, value in value.items():
252 member_shape = members[key]
253 member_prefix = self._get_serialized_name(member_shape, key)
254 if prefix:
255 member_prefix = f'{prefix}.{member_prefix}'
256 self._serialize(serialized, value, member_shape, member_prefix)
257
258 def _serialize_type_list(self, serialized, value, shape, prefix=''):
259 if not value:
260 # The query protocol serializes empty lists.
261 serialized[prefix] = ''
262 return
263 if self._is_shape_flattened(shape):
264 list_prefix = prefix
265 if shape.member.serialization.get('name'):
266 name = self._get_serialized_name(shape.member, default_name='')
267 # Replace '.Original' with '.{name}'.
268 list_prefix = '.'.join(prefix.split('.')[:-1] + [name])
269 else:
270 list_name = shape.member.serialization.get('name', 'member')
271 list_prefix = f'{prefix}.{list_name}'
272 for i, element in enumerate(value, 1):
273 element_prefix = f'{list_prefix}.{i}'
274 element_shape = shape.member
275 self._serialize(serialized, element, element_shape, element_prefix)
276
277 def _serialize_type_map(self, serialized, value, shape, prefix=''):
278 if self._is_shape_flattened(shape):
279 full_prefix = prefix
280 else:
281 full_prefix = f'{prefix}.entry'
282 template = full_prefix + '.{i}.{suffix}'
283 key_shape = shape.key
284 value_shape = shape.value
285 key_suffix = self._get_serialized_name(key_shape, default_name='key')
286 value_suffix = self._get_serialized_name(value_shape, 'value')
287 for i, key in enumerate(value, 1):
288 key_prefix = template.format(i=i, suffix=key_suffix)
289 value_prefix = template.format(i=i, suffix=value_suffix)
290 self._serialize(serialized, key, key_shape, key_prefix)
291 self._serialize(serialized, value[key], value_shape, value_prefix)
292
293 def _serialize_type_blob(self, serialized, value, shape, prefix=''):
294 # Blob args must be base64 encoded.
295 serialized[prefix] = self._get_base64(value)
296
297 def _serialize_type_timestamp(self, serialized, value, shape, prefix=''):
298 serialized[prefix] = self._convert_timestamp_to_str(
299 value, shape.serialization.get('timestampFormat')
300 )
301
302 def _serialize_type_boolean(self, serialized, value, shape, prefix=''):
303 if value:
304 serialized[prefix] = 'true'
305 else:
306 serialized[prefix] = 'false'
307
308 def _default_serialize(self, serialized, value, shape, prefix=''):
309 serialized[prefix] = value
310
311 def _is_shape_flattened(self, shape):
312 return shape.serialization.get('flattened')
313
314
315class EC2Serializer(QuerySerializer):
316 """EC2 specific customizations to the query protocol serializers.
317
318 The EC2 model is almost, but not exactly, similar to the query protocol
319 serializer. This class encapsulates those differences. The model
320 will have be marked with a ``protocol`` of ``ec2``, so you don't need
321 to worry about wiring this class up correctly.
322
323 """
324
325 def _get_serialized_name(self, shape, default_name):
326 # Returns the serialized name for the shape if it exists.
327 # Otherwise it will return the passed in default_name.
328 if 'queryName' in shape.serialization:
329 return shape.serialization['queryName']
330 elif 'name' in shape.serialization:
331 # A locationName is always capitalized
332 # on input for the ec2 protocol.
333 name = shape.serialization['name']
334 return name[0].upper() + name[1:]
335 else:
336 return default_name
337
338 def _serialize_type_list(self, serialized, value, shape, prefix=''):
339 for i, element in enumerate(value, 1):
340 element_prefix = f'{prefix}.{i}'
341 element_shape = shape.member
342 self._serialize(serialized, element, element_shape, element_prefix)
343
344
345class JSONSerializer(Serializer):
346 TIMESTAMP_FORMAT = 'unixtimestamp'
347
348 def serialize_to_request(self, parameters, operation_model):
349 target = '{}.{}'.format(
350 operation_model.metadata['targetPrefix'],
351 operation_model.name,
352 )
353 json_version = operation_model.metadata['jsonVersion']
354 serialized = self._create_default_request()
355 serialized['method'] = operation_model.http.get(
356 'method', self.DEFAULT_METHOD
357 )
358 serialized['headers'] = {
359 'X-Amz-Target': target,
360 'Content-Type': f'application/x-amz-json-{json_version}',
361 }
362 body = self.MAP_TYPE()
363 input_shape = operation_model.input_shape
364 if input_shape is not None:
365 self._serialize(body, parameters, input_shape)
366 serialized['body'] = json.dumps(body).encode(self.DEFAULT_ENCODING)
367
368 host_prefix = self._expand_host_prefix(parameters, operation_model)
369 if host_prefix is not None:
370 serialized['host_prefix'] = host_prefix
371
372 return serialized
373
374 def _serialize(self, serialized, value, shape, key=None):
375 method = getattr(
376 self,
377 f'_serialize_type_{shape.type_name}',
378 self._default_serialize,
379 )
380 method(serialized, value, shape, key)
381
382 def _serialize_type_structure(self, serialized, value, shape, key):
383 if shape.is_document_type:
384 serialized[key] = value
385 else:
386 if key is not None:
387 # If a key is provided, this is a result of a recursive
388 # call so we need to add a new child dict as the value
389 # of the passed in serialized dict. We'll then add
390 # all the structure members as key/vals in the new serialized
391 # dictionary we just created.
392 new_serialized = self.MAP_TYPE()
393 serialized[key] = new_serialized
394 serialized = new_serialized
395 members = shape.members
396 for member_key, member_value in value.items():
397 member_shape = members[member_key]
398 if 'name' in member_shape.serialization:
399 member_key = member_shape.serialization['name']
400 self._serialize(
401 serialized, member_value, member_shape, member_key
402 )
403
404 def _serialize_type_map(self, serialized, value, shape, key):
405 map_obj = self.MAP_TYPE()
406 serialized[key] = map_obj
407 for sub_key, sub_value in value.items():
408 self._serialize(map_obj, sub_value, shape.value, sub_key)
409
410 def _serialize_type_list(self, serialized, value, shape, key):
411 list_obj = []
412 serialized[key] = list_obj
413 for list_item in value:
414 wrapper = {}
415 # The JSON list serialization is the only case where we aren't
416 # setting a key on a dict. We handle this by using
417 # a __current__ key on a wrapper dict to serialize each
418 # list item before appending it to the serialized list.
419 self._serialize(wrapper, list_item, shape.member, "__current__")
420 list_obj.append(wrapper["__current__"])
421
422 def _default_serialize(self, serialized, value, shape, key):
423 serialized[key] = value
424
425 def _serialize_type_timestamp(self, serialized, value, shape, key):
426 serialized[key] = self._convert_timestamp_to_str(
427 value, shape.serialization.get('timestampFormat')
428 )
429
430 def _serialize_type_blob(self, serialized, value, shape, key):
431 serialized[key] = self._get_base64(value)
432
433
434class BaseRestSerializer(Serializer):
435 """Base class for rest protocols.
436
437 The only variance between the various rest protocols is the
438 way that the body is serialized. All other aspects (headers, uri, etc.)
439 are the same and logic for serializing those aspects lives here.
440
441 Subclasses must implement the ``_serialize_body_params`` method.
442
443 """
444
445 QUERY_STRING_TIMESTAMP_FORMAT = 'iso8601'
446 HEADER_TIMESTAMP_FORMAT = 'rfc822'
447 # This is a list of known values for the "location" key in the
448 # serialization dict. The location key tells us where on the request
449 # to put the serialized value.
450 KNOWN_LOCATIONS = ['uri', 'querystring', 'header', 'headers']
451
452 def serialize_to_request(self, parameters, operation_model):
453 serialized = self._create_default_request()
454 serialized['method'] = operation_model.http.get(
455 'method', self.DEFAULT_METHOD
456 )
457 shape = operation_model.input_shape
458 if shape is None:
459 serialized['url_path'] = operation_model.http['requestUri']
460 return serialized
461 shape_members = shape.members
462 # While the ``serialized`` key holds the final serialized request
463 # data, we need interim dicts for the various locations of the
464 # request. We need this for the uri_path_kwargs and the
465 # query_string_kwargs because they are templated, so we need
466 # to gather all the needed data for the string template,
467 # then we render the template. The body_kwargs is needed
468 # because once we've collected them all, we run them through
469 # _serialize_body_params, which for rest-json, creates JSON,
470 # and for rest-xml, will create XML. This is what the
471 # ``partitioned`` dict below is for.
472 partitioned = {
473 'uri_path_kwargs': self.MAP_TYPE(),
474 'query_string_kwargs': self.MAP_TYPE(),
475 'body_kwargs': self.MAP_TYPE(),
476 'headers': self.MAP_TYPE(),
477 }
478 for param_name, param_value in parameters.items():
479 if param_value is None:
480 # Don't serialize any parameter with a None value.
481 continue
482 self._partition_parameters(
483 partitioned, param_name, param_value, shape_members
484 )
485 serialized['url_path'] = self._render_uri_template(
486 operation_model.http['requestUri'], partitioned['uri_path_kwargs']
487 )
488
489 if 'authPath' in operation_model.http:
490 serialized['auth_path'] = self._render_uri_template(
491 operation_model.http['authPath'],
492 partitioned['uri_path_kwargs'],
493 )
494 # Note that we lean on the http implementation to handle the case
495 # where the requestUri path already has query parameters.
496 # The bundled http client, requests, already supports this.
497 serialized['query_string'] = partitioned['query_string_kwargs']
498 if partitioned['headers']:
499 serialized['headers'] = partitioned['headers']
500 self._serialize_payload(
501 partitioned, parameters, serialized, shape, shape_members
502 )
503 self._serialize_content_type(serialized, shape, shape_members)
504
505 host_prefix = self._expand_host_prefix(parameters, operation_model)
506 if host_prefix is not None:
507 serialized['host_prefix'] = host_prefix
508
509 return serialized
510
511 def _render_uri_template(self, uri_template, params):
512 # We need to handle two cases::
513 #
514 # /{Bucket}/foo
515 # /{Key+}/bar
516 # A label ending with '+' is greedy. There can only
517 # be one greedy key.
518 encoded_params = {}
519 for template_param in re.findall(r'{(.*?)}', uri_template):
520 if template_param.endswith('+'):
521 encoded_params[template_param] = percent_encode(
522 params[template_param[:-1]], safe='/~'
523 )
524 else:
525 encoded_params[template_param] = percent_encode(
526 params[template_param]
527 )
528 return uri_template.format(**encoded_params)
529
530 def _serialize_payload(
531 self, partitioned, parameters, serialized, shape, shape_members
532 ):
533 # partitioned - The user input params partitioned by location.
534 # parameters - The user input params.
535 # serialized - The final serialized request dict.
536 # shape - Describes the expected input shape
537 # shape_members - The members of the input struct shape
538 payload_member = shape.serialization.get('payload')
539 if self._has_streaming_payload(payload_member, shape_members):
540 # If it's streaming, then the body is just the
541 # value of the payload.
542 body_payload = parameters.get(payload_member, b'')
543 body_payload = self._encode_payload(body_payload)
544 serialized['body'] = body_payload
545 elif payload_member is not None:
546 # If there's a payload member, we serialized that
547 # member to they body.
548 body_params = parameters.get(payload_member)
549 if body_params is not None:
550 serialized['body'] = self._serialize_body_params(
551 body_params, shape_members[payload_member]
552 )
553 else:
554 serialized['body'] = self._serialize_empty_body()
555 elif partitioned['body_kwargs']:
556 serialized['body'] = self._serialize_body_params(
557 partitioned['body_kwargs'], shape
558 )
559 elif self._requires_empty_body(shape):
560 serialized['body'] = self._serialize_empty_body()
561
562 def _serialize_empty_body(self):
563 return b''
564
565 def _serialize_content_type(self, serialized, shape, shape_members):
566 """
567 Some protocols require varied Content-Type headers
568 depending on user input. This allows subclasses to apply
569 this conditionally.
570 """
571 pass
572
573 def _requires_empty_body(self, shape):
574 """
575 Some protocols require a specific body to represent an empty
576 payload. This allows subclasses to apply this conditionally.
577 """
578 return False
579
580 def _has_streaming_payload(self, payload, shape_members):
581 """Determine if payload is streaming (a blob or string)."""
582 return payload is not None and shape_members[payload].type_name in (
583 'blob',
584 'string',
585 )
586
587 def _encode_payload(self, body):
588 if isinstance(body, str):
589 return body.encode(self.DEFAULT_ENCODING)
590 return body
591
592 def _partition_parameters(
593 self, partitioned, param_name, param_value, shape_members
594 ):
595 # This takes the user provided input parameter (``param``)
596 # and figures out where they go in the request dict.
597 # Some params are HTTP headers, some are used in the URI, some
598 # are in the request body. This method deals with this.
599 member = shape_members[param_name]
600 location = member.serialization.get('location')
601 key_name = member.serialization.get('name', param_name)
602 if location == 'uri':
603 partitioned['uri_path_kwargs'][key_name] = param_value
604 elif location == 'querystring':
605 if isinstance(param_value, dict):
606 partitioned['query_string_kwargs'].update(param_value)
607 elif isinstance(param_value, bool):
608 bool_str = str(param_value).lower()
609 partitioned['query_string_kwargs'][key_name] = bool_str
610 elif member.type_name == 'timestamp':
611 timestamp_format = member.serialization.get(
612 'timestampFormat', self.QUERY_STRING_TIMESTAMP_FORMAT
613 )
614 timestamp = self._convert_timestamp_to_str(
615 param_value, timestamp_format
616 )
617 partitioned['query_string_kwargs'][key_name] = timestamp
618 else:
619 partitioned['query_string_kwargs'][key_name] = param_value
620 elif location == 'header':
621 shape = shape_members[param_name]
622 if not param_value and shape.type_name == 'list':
623 # Empty lists should not be set on the headers
624 return
625 value = self._convert_header_value(shape, param_value)
626 partitioned['headers'][key_name] = str(value)
627 elif location == 'headers':
628 # 'headers' is a bit of an oddball. The ``key_name``
629 # is actually really a prefix for the header names:
630 header_prefix = key_name
631 # The value provided by the user is a dict so we'll be
632 # creating multiple header key/val pairs. The key
633 # name to use for each header is the header_prefix (``key_name``)
634 # plus the key provided by the user.
635 self._do_serialize_header_map(
636 header_prefix, partitioned['headers'], param_value
637 )
638 else:
639 partitioned['body_kwargs'][param_name] = param_value
640
641 def _do_serialize_header_map(self, header_prefix, headers, user_input):
642 for key, val in user_input.items():
643 full_key = header_prefix + key
644 headers[full_key] = val
645
646 def _serialize_body_params(self, params, shape):
647 raise NotImplementedError('_serialize_body_params')
648
649 def _convert_header_value(self, shape, value):
650 if shape.type_name == 'timestamp':
651 datetime_obj = parse_to_aware_datetime(value)
652 timestamp = calendar.timegm(datetime_obj.utctimetuple())
653 timestamp_format = shape.serialization.get(
654 'timestampFormat', self.HEADER_TIMESTAMP_FORMAT
655 )
656 return self._convert_timestamp_to_str(timestamp, timestamp_format)
657 elif shape.type_name == 'list':
658 converted_value = [
659 self._convert_header_value(shape.member, v)
660 for v in value
661 if v is not None
662 ]
663 return ",".join(converted_value)
664 elif is_json_value_header(shape):
665 # Serialize with no spaces after separators to save space in
666 # the header.
667 return self._get_base64(json.dumps(value, separators=(',', ':')))
668 else:
669 return value
670
671
672class RestJSONSerializer(BaseRestSerializer, JSONSerializer):
673 def _serialize_empty_body(self):
674 return b'{}'
675
676 def _requires_empty_body(self, shape):
677 """
678 Serialize an empty JSON object whenever the shape has
679 members not targeting a location.
680 """
681 for member, val in shape.members.items():
682 if 'location' not in val.serialization:
683 return True
684 return False
685
686 def _serialize_content_type(self, serialized, shape, shape_members):
687 """Set Content-Type to application/json for all structured bodies."""
688 payload = shape.serialization.get('payload')
689 if self._has_streaming_payload(payload, shape_members):
690 # Don't apply content-type to streaming bodies
691 return
692
693 has_body = serialized['body'] != b''
694 has_content_type = has_header('Content-Type', serialized['headers'])
695 if has_body and not has_content_type:
696 serialized['headers']['Content-Type'] = 'application/json'
697
698 def _serialize_body_params(self, params, shape):
699 serialized_body = self.MAP_TYPE()
700 self._serialize(serialized_body, params, shape)
701 return json.dumps(serialized_body).encode(self.DEFAULT_ENCODING)
702
703
704class RestXMLSerializer(BaseRestSerializer):
705 TIMESTAMP_FORMAT = 'iso8601'
706
707 def _serialize_body_params(self, params, shape):
708 root_name = shape.serialization['name']
709 pseudo_root = ElementTree.Element('')
710 self._serialize(shape, params, pseudo_root, root_name)
711 real_root = list(pseudo_root)[0]
712 return ElementTree.tostring(real_root, encoding=self.DEFAULT_ENCODING)
713
714 def _serialize(self, shape, params, xmlnode, name):
715 method = getattr(
716 self,
717 f'_serialize_type_{shape.type_name}',
718 self._default_serialize,
719 )
720 method(xmlnode, params, shape, name)
721
722 def _serialize_type_structure(self, xmlnode, params, shape, name):
723 structure_node = ElementTree.SubElement(xmlnode, name)
724
725 if 'xmlNamespace' in shape.serialization:
726 namespace_metadata = shape.serialization['xmlNamespace']
727 attribute_name = 'xmlns'
728 if namespace_metadata.get('prefix'):
729 attribute_name += f":{namespace_metadata['prefix']}"
730 structure_node.attrib[attribute_name] = namespace_metadata['uri']
731 for key, value in params.items():
732 member_shape = shape.members[key]
733 member_name = member_shape.serialization.get('name', key)
734 # We need to special case member shapes that are marked as an
735 # xmlAttribute. Rather than serializing into an XML child node,
736 # we instead serialize the shape to an XML attribute of the
737 # *current* node.
738 if value is None:
739 # Don't serialize any param whose value is None.
740 return
741 if member_shape.serialization.get('xmlAttribute'):
742 # xmlAttributes must have a serialization name.
743 xml_attribute_name = member_shape.serialization['name']
744 structure_node.attrib[xml_attribute_name] = value
745 continue
746 self._serialize(member_shape, value, structure_node, member_name)
747
748 def _serialize_type_list(self, xmlnode, params, shape, name):
749 member_shape = shape.member
750 if shape.serialization.get('flattened'):
751 element_name = name
752 list_node = xmlnode
753 else:
754 element_name = member_shape.serialization.get('name', 'member')
755 list_node = ElementTree.SubElement(xmlnode, name)
756 for item in params:
757 self._serialize(member_shape, item, list_node, element_name)
758
759 def _serialize_type_map(self, xmlnode, params, shape, name):
760 # Given the ``name`` of MyMap, and input of {"key1": "val1"}
761 # we serialize this as:
762 # <MyMap>
763 # <entry>
764 # <key>key1</key>
765 # <value>val1</value>
766 # </entry>
767 # </MyMap>
768 node = ElementTree.SubElement(xmlnode, name)
769 # TODO: handle flattened maps.
770 for key, value in params.items():
771 entry_node = ElementTree.SubElement(node, 'entry')
772 key_name = self._get_serialized_name(shape.key, default_name='key')
773 val_name = self._get_serialized_name(
774 shape.value, default_name='value'
775 )
776 self._serialize(shape.key, key, entry_node, key_name)
777 self._serialize(shape.value, value, entry_node, val_name)
778
779 def _serialize_type_boolean(self, xmlnode, params, shape, name):
780 # For scalar types, the 'params' attr is actually just a scalar
781 # value representing the data we need to serialize as a boolean.
782 # It will either be 'true' or 'false'
783 node = ElementTree.SubElement(xmlnode, name)
784 if params:
785 str_value = 'true'
786 else:
787 str_value = 'false'
788 node.text = str_value
789
790 def _serialize_type_blob(self, xmlnode, params, shape, name):
791 node = ElementTree.SubElement(xmlnode, name)
792 node.text = self._get_base64(params)
793
794 def _serialize_type_timestamp(self, xmlnode, params, shape, name):
795 node = ElementTree.SubElement(xmlnode, name)
796 node.text = self._convert_timestamp_to_str(
797 params, shape.serialization.get('timestampFormat')
798 )
799
800 def _default_serialize(self, xmlnode, params, shape, name):
801 node = ElementTree.SubElement(xmlnode, name)
802 node.text = str(params)
803
804
805SERIALIZERS = {
806 'ec2': EC2Serializer,
807 'query': QuerySerializer,
808 'json': JSONSerializer,
809 'rest-json': RestJSONSerializer,
810 'rest-xml': RestXMLSerializer,
811}