1# Copyright 2021 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Helpers for rest transports."""
16
17import functools
18import operator
19
20
21def flatten_query_params(obj):
22    """Flatten a nested dict into a list of (name,value) tuples.
23
24    The result is suitable for setting query params on an http request.
25
26    .. code-block:: python
27
28        >>> obj = {'a':
29        ...         {'b':
30        ...           {'c': ['x', 'y', 'z']} },
31        ...      'd': 'uvw', }
32        >>> flatten_query_params(obj)
33        [('a.b.c', 'x'), ('a.b.c', 'y'), ('a.b.c', 'z'), ('d', 'uvw')]
34
35    Note that, as described in
36    https://github.com/googleapis/googleapis/blob/48d9fb8c8e287c472af500221c6450ecd45d7d39/google/api/http.proto#L117,
37    repeated fields (i.e. list-valued fields) may only contain primitive types (not lists or dicts).
38    This is enforced in this function.
39
40    Args:
41      obj: a nested dictionary (from json), or None
42
43    Returns: a list of tuples, with each tuple having a (possibly) multi-part name
44      and a scalar value.
45
46    Raises:
47      TypeError if obj is not a dict or None
48      ValueError if obj contains a list of non-primitive values.
49    """
50
51    if obj is not None and not isinstance(obj, dict):
52        raise TypeError("flatten_query_params must be called with dict object")
53
54    return _flatten(obj, key_path=[])
55
56
57def _flatten(obj, key_path):
58    if obj is None:
59        return []
60    if isinstance(obj, dict):
61        return _flatten_dict(obj, key_path=key_path)
62    if isinstance(obj, list):
63        return _flatten_list(obj, key_path=key_path)
64    return _flatten_value(obj, key_path=key_path)
65
66
67def _is_primitive_value(obj):
68    if obj is None:
69        return False
70
71    if isinstance(obj, (list, dict)):
72        raise ValueError("query params may not contain repeated dicts or lists")
73
74    return True
75
76
77def _flatten_value(obj, key_path):
78    return [(".".join(key_path), obj)]
79
80
81def _flatten_dict(obj, key_path):
82    items = (_flatten(value, key_path=key_path + [key]) for key, value in obj.items())
83    return functools.reduce(operator.concat, items, [])
84
85
86def _flatten_list(elems, key_path):
87    # Only lists of scalar values are supported.
88    # The name (key_path) is repeated for each value.
89    items = (
90        _flatten_value(elem, key_path=key_path)
91        for elem in elems
92        if _is_primitive_value(elem)
93    )
94    return functools.reduce(operator.concat, items, [])
95