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
14import jmespath
15from botocore import xform_name
16
17from .params import get_data_member
18
19
20def all_not_none(iterable):
21    """
22    Return True if all elements of the iterable are not None (or if the
23    iterable is empty). This is like the built-in ``all``, except checks
24    against None, so 0 and False are allowable values.
25    """
26    for element in iterable:
27        if element is None:
28            return False
29    return True
30
31
32def build_identifiers(identifiers, parent, params=None, raw_response=None):
33    """
34    Builds a mapping of identifier names to values based on the
35    identifier source location, type, and target. Identifier
36    values may be scalars or lists depending on the source type
37    and location.
38
39    :type identifiers: list
40    :param identifiers: List of :py:class:`~boto3.resources.model.Parameter`
41                        definitions
42    :type parent: ServiceResource
43    :param parent: The resource instance to which this action is attached.
44    :type params: dict
45    :param params: Request parameters sent to the service.
46    :type raw_response: dict
47    :param raw_response: Low-level operation response.
48    :rtype: list
49    :return: An ordered list of ``(name, value)`` identifier tuples.
50    """
51    results = []
52
53    for identifier in identifiers:
54        source = identifier.source
55        target = identifier.target
56
57        if source == 'response':
58            value = jmespath.search(identifier.path, raw_response)
59        elif source == 'requestParameter':
60            value = jmespath.search(identifier.path, params)
61        elif source == 'identifier':
62            value = getattr(parent, xform_name(identifier.name))
63        elif source == 'data':
64            # If this is a data member then it may incur a load
65            # action before returning the value.
66            value = get_data_member(parent, identifier.path)
67        elif source == 'input':
68            # This value is set by the user, so ignore it here
69            continue
70        else:
71            raise NotImplementedError(
72                'Unsupported source type: {0}'.format(source))
73
74        results.append((xform_name(target), value))
75
76    return results
77
78
79def build_empty_response(search_path, operation_name, service_model):
80    """
81    Creates an appropriate empty response for the type that is expected,
82    based on the service model's shape type. For example, a value that
83    is normally a list would then return an empty list. A structure would
84    return an empty dict, and a number would return None.
85
86    :type search_path: string
87    :param search_path: JMESPath expression to search in the response
88    :type operation_name: string
89    :param operation_name: Name of the underlying service operation.
90    :type service_model: :ref:`botocore.model.ServiceModel`
91    :param service_model: The Botocore service model
92    :rtype: dict, list, or None
93    :return: An appropriate empty value
94    """
95    response = None
96
97    operation_model = service_model.operation_model(operation_name)
98    shape = operation_model.output_shape
99
100    if search_path:
101        # Walk the search path and find the final shape. For example, given
102        # a path of ``foo.bar[0].baz``, we first find the shape for ``foo``,
103        # then the shape for ``bar`` (ignoring the indexing), and finally
104        # the shape for ``baz``.
105        for item in search_path.split('.'):
106            item = item.strip('[0123456789]$')
107
108            if shape.type_name == 'structure':
109                shape = shape.members[item]
110            elif shape.type_name == 'list':
111                shape = shape.member
112            else:
113                raise NotImplementedError(
114                    'Search path hits shape type {0} from {1}'.format(
115                        shape.type_name, item))
116
117    # Anything not handled here is set to None
118    if shape.type_name == 'structure':
119        response = {}
120    elif shape.type_name == 'list':
121        response = []
122    elif shape.type_name == 'map':
123        response = {}
124
125    return response
126
127
128class RawHandler(object):
129    """
130    A raw action response handler. This passed through the response
131    dictionary, optionally after performing a JMESPath search if one
132    has been defined for the action.
133
134    :type search_path: string
135    :param search_path: JMESPath expression to search in the response
136    :rtype: dict
137    :return: Service response
138    """
139    def __init__(self, search_path):
140        self.search_path = search_path
141
142    def __call__(self, parent, params, response):
143        """
144        :type parent: ServiceResource
145        :param parent: The resource instance to which this action is attached.
146        :type params: dict
147        :param params: Request parameters sent to the service.
148        :type response: dict
149        :param response: Low-level operation response.
150        """
151        # TODO: Remove the '$' check after JMESPath supports it
152        if self.search_path and self.search_path != '$':
153            response = jmespath.search(self.search_path, response)
154
155        return response
156
157
158class ResourceHandler(object):
159    """
160    Creates a new resource or list of new resources from the low-level
161    response based on the given response resource definition.
162
163    :type search_path: string
164    :param search_path: JMESPath expression to search in the response
165
166    :type factory: ResourceFactory
167    :param factory: The factory that created the resource class to which
168                    this action is attached.
169
170    :type resource_model: :py:class:`~boto3.resources.model.ResponseResource`
171    :param resource_model: Response resource model.
172
173    :type service_context: :py:class:`~boto3.utils.ServiceContext`
174    :param service_context: Context about the AWS service
175
176    :type operation_name: string
177    :param operation_name: Name of the underlying service operation, if it
178                           exists.
179
180    :rtype: ServiceResource or list
181    :return: New resource instance(s).
182    """
183    def __init__(self, search_path, factory, resource_model,
184                 service_context, operation_name=None):
185        self.search_path = search_path
186        self.factory = factory
187        self.resource_model = resource_model
188        self.operation_name = operation_name
189        self.service_context = service_context
190
191    def __call__(self, parent, params, response):
192        """
193        :type parent: ServiceResource
194        :param parent: The resource instance to which this action is attached.
195        :type params: dict
196        :param params: Request parameters sent to the service.
197        :type response: dict
198        :param response: Low-level operation response.
199        """
200        resource_name = self.resource_model.type
201        json_definition = self.service_context.resource_json_definitions.get(
202            resource_name)
203
204        # Load the new resource class that will result from this action.
205        resource_cls = self.factory.load_from_definition(
206            resource_name=resource_name,
207            single_resource_json_definition=json_definition,
208            service_context=self.service_context
209        )
210        raw_response = response
211        search_response = None
212
213        # Anytime a path is defined, it means the response contains the
214        # resource's attributes, so resource_data gets set here. It
215        # eventually ends up in resource.meta.data, which is where
216        # the attribute properties look for data.
217        if self.search_path:
218            search_response = jmespath.search(self.search_path, raw_response)
219
220        # First, we parse all the identifiers, then create the individual
221        # response resources using them. Any identifiers that are lists
222        # will have one item consumed from the front of the list for each
223        # resource that is instantiated. Items which are not a list will
224        # be set as the same value on each new resource instance.
225        identifiers = dict(build_identifiers(
226            self.resource_model.identifiers, parent, params,
227            raw_response))
228
229        # If any of the identifiers is a list, then the response is plural
230        plural = [v for v in identifiers.values() if isinstance(v, list)]
231
232        if plural:
233            response = []
234
235            # The number of items in an identifier that is a list will
236            # determine how many resource instances to create.
237            for i in range(len(plural[0])):
238                # Response item data is *only* available if a search path
239                # was given. This prevents accidentally loading unrelated
240                # data that may be in the response.
241                response_item = None
242                if search_response:
243                    response_item = search_response[i]
244                response.append(
245                    self.handle_response_item(resource_cls, parent,
246                                              identifiers, response_item))
247        elif all_not_none(identifiers.values()):
248            # All identifiers must always exist, otherwise the resource
249            # cannot be instantiated.
250            response = self.handle_response_item(
251                resource_cls, parent, identifiers, search_response)
252        else:
253            # The response should be empty, but that may mean an
254            # empty dict, list, or None based on whether we make
255            # a remote service call and what shape it is expected
256            # to return.
257            response = None
258            if self.operation_name is not None:
259                # A remote service call was made, so try and determine
260                # its shape.
261                response = build_empty_response(
262                    self.search_path, self.operation_name,
263                    self.service_context.service_model)
264
265        return response
266
267    def handle_response_item(self, resource_cls, parent, identifiers,
268                             resource_data):
269        """
270        Handles the creation of a single response item by setting
271        parameters and creating the appropriate resource instance.
272
273        :type resource_cls: ServiceResource subclass
274        :param resource_cls: The resource class to instantiate.
275        :type parent: ServiceResource
276        :param parent: The resource instance to which this action is attached.
277        :type identifiers: dict
278        :param identifiers: Map of identifier names to value or values.
279        :type resource_data: dict or None
280        :param resource_data: Data for resource attributes.
281        :rtype: ServiceResource
282        :return: New resource instance.
283        """
284        kwargs = {
285            'client': parent.meta.client,
286        }
287
288        for name, value in identifiers.items():
289            # If value is a list, then consume the next item
290            if isinstance(value, list):
291                value = value.pop(0)
292
293            kwargs[name] = value
294
295        resource = resource_cls(**kwargs)
296
297        if resource_data is not None:
298            resource.meta.data = resource_data
299
300        return resource
301