blob: 19519db6bfa64c0a379a4b21c686e9cb0cd66c42 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 Google Inc. All Rights Reserved.
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"""Module for a class that contains a request body resource and parameters."""
16from __future__ import absolute_import
17
18from . import message_types
19from . import messages
20
21
22class ResourceContainer(object):
23 """Container for a request body resource combined with parameters.
24
25 Used for API methods which may also have path or query parameters in addition
26 to a request body.
27
28 Attributes:
29 body_message_class: A message class to represent a request body.
30 parameters_message_class: A placeholder message class for request
31 parameters.
32 """
33
34 __remote_info_cache = {} # pylint: disable=g-bad-name
35
36 __combined_message_class = None # pylint: disable=invalid-name
37
38 def __init__(self, _body_message_class=message_types.VoidMessage, **kwargs):
39 """Constructor for ResourceContainer.
40
41 Stores a request body message class and attempts to create one from the
42 keyword arguments passed in.
43
44 Args:
45 _body_message_class: A keyword argument to be treated like a positional
46 argument. This will not conflict with the potential names of fields
47 since they can't begin with underscore. We make this a keyword
48 argument since the default VoidMessage is a very common choice given
49 the prevalence of GET methods.
50 **kwargs: Keyword arguments specifying field names (the named arguments)
51 and instances of ProtoRPC fields as the values.
52 """
53 self.body_message_class = _body_message_class
54 self.parameters_message_class = type('ParameterContainer',
55 (messages.Message,), kwargs)
56
57 @property
58 def combined_message_class(self):
59 """A ProtoRPC message class with both request and parameters fields.
60
61 Caches the result in a local private variable. Uses _CopyField to create
62 copies of the fields from the existing request and parameters classes since
63 those fields are "owned" by the message classes.
64
65 Raises:
66 TypeError: If a field name is used in both the request message and the
67 parameters but the two fields do not represent the same type.
68
69 Returns:
70 Value of combined message class for this property.
71 """
72 if self.__combined_message_class is not None:
73 return self.__combined_message_class
74
75 fields = {}
76 # We don't need to preserve field.number since this combined class is only
77 # used for the protorpc remote.method and is not needed for the API config.
78 # The only place field.number matters is in parameterOrder, but this is set
79 # based on container.parameters_message_class which will use the field
80 # numbers originally passed in.
81
82 # Counter for fields.
83 field_number = 1
84 for field in self.body_message_class.all_fields():
85 fields[field.name] = _CopyField(field, number=field_number)
86 field_number += 1
87 for field in self.parameters_message_class.all_fields():
88 if field.name in fields:
89 if not _CompareFields(field, fields[field.name]):
90 raise TypeError('Field %r contained in both parameters and request '
91 'body, but the fields differ.' % (field.name,))
92 else:
93 # Skip a field that's already there.
94 continue
95 fields[field.name] = _CopyField(field, number=field_number)
96 field_number += 1
97
98 self.__combined_message_class = type('CombinedContainer',
99 (messages.Message,), fields)
100 return self.__combined_message_class
101
102 @classmethod
103 def add_to_cache(cls, remote_info, container): # pylint: disable=g-bad-name
104 """Adds a ResourceContainer to a cache tying it to a protorpc method.
105
106 Args:
107 remote_info: Instance of protorpc.remote._RemoteMethodInfo corresponding
108 to a method.
109 container: An instance of ResourceContainer.
110
111 Raises:
112 TypeError: if the container is not an instance of cls.
113 KeyError: if the remote method has been reference by a container before.
114 This created remote method should never occur because a remote method
115 is created once.
116 """
117 if not isinstance(container, cls):
118 raise TypeError('%r not an instance of %r, could not be added to cache.' %
119 (container, cls))
120 if remote_info in cls.__remote_info_cache:
121 raise KeyError('Cache has collision but should not.')
122 cls.__remote_info_cache[remote_info] = container
123
124 @classmethod
125 def get_request_message(cls, remote_info): # pylint: disable=g-bad-name
126 """Gets request message or container from remote info.
127
128 Args:
129 remote_info: Instance of protorpc.remote._RemoteMethodInfo corresponding
130 to a method.
131
132 Returns:
133 Either an instance of the request type from the remote or the
134 ResourceContainer that was cached with the remote method.
135 """
136 if remote_info in cls.__remote_info_cache:
137 return cls.__remote_info_cache[remote_info]
138 else:
139 return remote_info.request_type()
140
141
142def _GetFieldAttributes(field):
143 """Decomposes field into the needed arguments to pass to the constructor.
144
145 This can be used to create copies of the field or to compare if two fields
146 are "equal" (since __eq__ is not implemented on messages.Field).
147
148 Args:
149 field: A ProtoRPC message field (potentially to be copied).
150
151 Raises:
152 TypeError: If the field is not an instance of messages.Field.
153
154 Returns:
155 A pair of relevant arguments to be passed to the constructor for the field
156 type. The first element is a list of positional arguments for the
157 constructor and the second is a dictionary of keyword arguments.
158 """
159 if not isinstance(field, messages.Field):
160 raise TypeError('Field %r to be copied not a ProtoRPC field.' % (field,))
161
162 positional_args = []
163 kwargs = {
164 'required': field.required,
165 'repeated': field.repeated,
166 'variant': field.variant,
167 'default': field._Field__default, # pylint: disable=protected-access
168 }
169
170 if isinstance(field, messages.MessageField):
171 # Message fields can't have a default
172 kwargs.pop('default')
173 if not isinstance(field, message_types.DateTimeField):
174 positional_args.insert(0, field.message_type)
175 elif isinstance(field, messages.EnumField):
176 positional_args.insert(0, field.type)
177
178 return positional_args, kwargs
179
180
181def _CompareFields(field, other_field):
182 """Checks if two ProtoRPC fields are "equal".
183
184 Compares the arguments, rather than the id of the elements (which is
185 the default __eq__ behavior) as well as the class of the fields.
186
187 Args:
188 field: A ProtoRPC message field to be compared.
189 other_field: A ProtoRPC message field to be compared.
190
191 Returns:
192 Boolean indicating whether the fields are equal.
193 """
194 field_attrs = _GetFieldAttributes(field)
195 other_field_attrs = _GetFieldAttributes(other_field)
196 if field_attrs != other_field_attrs:
197 return False
198 return field.__class__ == other_field.__class__
199
200
201def _CopyField(field, number=None):
202 """Copies a (potentially) owned ProtoRPC field instance into a new copy.
203
204 Args:
205 field: A ProtoRPC message field to be copied.
206 number: An integer for the field to override the number of the field.
207 Defaults to None.
208
209 Raises:
210 TypeError: If the field is not an instance of messages.Field.
211
212 Returns:
213 A copy of the ProtoRPC message field.
214 """
215 positional_args, kwargs = _GetFieldAttributes(field)
216 number = number or field.number
217 positional_args.append(number)
218 return field.__class__(*positional_args, **kwargs)