Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(20)

Side by Side Diff: third_party/google-endpoints/endpoints/discovery_generator.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 # 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 """A library for converting service configs to discovery docs."""
16
17 import collections
18 import json
19 import logging
20 import re
21
22 import api_exceptions
23 import message_parser
24 from protorpc import message_types
25 from protorpc import messages
26 from protorpc import remote
27 import resource_container
28 import util
29
30
31 _PATH_VARIABLE_PATTERN = r'{([a-zA-Z_][a-zA-Z_.\d]*)}'
32
33 _MULTICLASS_MISMATCH_ERROR_TEMPLATE = (
34 'Attempting to implement service %s, version %s, with multiple '
35 'classes that are not compatible. See docstring for api() for '
36 'examples how to implement a multi-class API.')
37
38 _INVALID_AUTH_ISSUER = 'No auth issuer named %s defined in this Endpoints API.'
39
40 _API_KEY = 'api_key'
41 _API_KEY_PARAM = 'key'
42
43 CUSTOM_VARIANT_MAP = {
44 messages.Variant.DOUBLE: ('number', 'double'),
45 messages.Variant.FLOAT: ('number', 'float'),
46 messages.Variant.INT64: ('string', 'int64'),
47 messages.Variant.SINT64: ('string', 'int64'),
48 messages.Variant.UINT64: ('string', 'uint64'),
49 messages.Variant.INT32: ('integer', 'int32'),
50 messages.Variant.SINT32: ('integer', 'int32'),
51 messages.Variant.UINT32: ('integer', 'uint32'),
52 messages.Variant.BOOL: ('boolean', None),
53 messages.Variant.STRING: ('string', None),
54 messages.Variant.BYTES: ('string', 'byte'),
55 messages.Variant.ENUM: ('string', None),
56 }
57
58
59
60 class DiscoveryGenerator(object):
61 """Generates a discovery doc from a ProtoRPC service.
62
63 Example:
64
65 class HelloRequest(messages.Message):
66 my_name = messages.StringField(1, required=True)
67
68 class HelloResponse(messages.Message):
69 hello = messages.StringField(1, required=True)
70
71 class HelloService(remote.Service):
72
73 @remote.method(HelloRequest, HelloResponse)
74 def hello(self, request):
75 return HelloResponse(hello='Hello there, %s!' %
76 request.my_name)
77
78 api_config = DiscoveryGenerator().pretty_print_config_to_json(HelloService)
79
80 The resulting api_config will be a JSON discovery document describing the API
81 implemented by HelloService.
82 """
83
84 # Constants for categorizing a request method.
85 # __NO_BODY - Request without a request body, such as GET and DELETE methods.
86 # __HAS_BODY - Request (such as POST/PUT/PATCH) with info in the request body.
87 __NO_BODY = 1 # pylint: disable=invalid-name
88 __HAS_BODY = 2 # pylint: disable=invalid-name
89
90 def __init__(self):
91 self.__parser = message_parser.MessageTypeToJsonSchema()
92
93 # Maps method id to the request schema id.
94 self.__request_schema = {}
95
96 # Maps method id to the response schema id.
97 self.__response_schema = {}
98
99 def _get_resource_path(self, method_id):
100 """Return the resource path for a method or an empty array if none."""
101 return method_id.split('.')[1:-1]
102
103 def _get_canonical_method_id(self, method_id):
104 return method_id.split('.')[-1]
105
106 def __get_request_kind(self, method_info):
107 """Categorize the type of the request.
108
109 Args:
110 method_info: _MethodInfo, method information.
111
112 Returns:
113 The kind of request.
114 """
115 if method_info.http_method in ('GET', 'DELETE'):
116 return self.__NO_BODY
117 else:
118 return self.__HAS_BODY
119
120 def __field_to_subfields(self, field):
121 """Fully describes data represented by field, including the nested case.
122
123 In the case that the field is not a message field, we have no fields nested
124 within a message definition, so we can simply return that field. However, in
125 the nested case, we can't simply describe the data with one field or even
126 with one chain of fields.
127
128 For example, if we have a message field
129
130 m_field = messages.MessageField(RefClass, 1)
131
132 which references a class with two fields:
133
134 class RefClass(messages.Message):
135 one = messages.StringField(1)
136 two = messages.IntegerField(2)
137
138 then we would need to include both one and two to represent all the
139 data contained.
140
141 Calling __field_to_subfields(m_field) would return:
142 [
143 [<MessageField "m_field">, <StringField "one">],
144 [<MessageField "m_field">, <StringField "two">],
145 ]
146
147 If the second field was instead a message field
148
149 class RefClass(messages.Message):
150 one = messages.StringField(1)
151 two = messages.MessageField(OtherRefClass, 2)
152
153 referencing another class with two fields
154
155 class OtherRefClass(messages.Message):
156 three = messages.BooleanField(1)
157 four = messages.FloatField(2)
158
159 then we would need to recurse one level deeper for two.
160
161 With this change, calling __field_to_subfields(m_field) would return:
162 [
163 [<MessageField "m_field">, <StringField "one">],
164 [<MessageField "m_field">, <StringField "two">, <StringField "three">],
165 [<MessageField "m_field">, <StringField "two">, <StringField "four">],
166 ]
167
168 Args:
169 field: An instance of a subclass of messages.Field.
170
171 Returns:
172 A list of lists, where each sublist is a list of fields.
173 """
174 # Termination condition
175 if not isinstance(field, messages.MessageField):
176 return [[field]]
177
178 result = []
179 for subfield in sorted(field.message_type.all_fields(),
180 key=lambda f: f.number):
181 subfield_results = self.__field_to_subfields(subfield)
182 for subfields_list in subfield_results:
183 subfields_list.insert(0, field)
184 result.append(subfields_list)
185 return result
186
187 def __field_to_parameter_type_and_format(self, field):
188 """Converts the field variant type into a tuple describing the parameter.
189
190 Args:
191 field: An instance of a subclass of messages.Field.
192
193 Returns:
194 A tuple with the type and format of the field, respectively.
195
196 Raises:
197 TypeError: if the field variant is a message variant.
198 """
199 # We use lowercase values for types (e.g. 'string' instead of 'STRING').
200 variant = field.variant
201 if variant == messages.Variant.MESSAGE:
202 raise TypeError('A message variant cannot be used in a parameter.')
203
204 # Note that the 64-bit integers are marked as strings -- this is to
205 # accommodate JavaScript, which would otherwise demote them to 32-bit
206 # integers.
207
208 return CUSTOM_VARIANT_MAP.get(variant) or (variant.name.lower(), None)
209
210 def __get_path_parameters(self, path):
211 """Parses path paremeters from a URI path and organizes them by parameter.
212
213 Some of the parameters may correspond to message fields, and so will be
214 represented as segments corresponding to each subfield; e.g. first.second if
215 the field "second" in the message field "first" is pulled from the path.
216
217 The resulting dictionary uses the first segments as keys and each key has as
218 value the list of full parameter values with first segment equal to the key.
219
220 If the match path parameter is null, that part of the path template is
221 ignored; this occurs if '{}' is used in a template.
222
223 Args:
224 path: String; a URI path, potentially with some parameters.
225
226 Returns:
227 A dictionary with strings as keys and list of strings as values.
228 """
229 path_parameters_by_segment = {}
230 for format_var_name in re.findall(_PATH_VARIABLE_PATTERN, path):
231 first_segment = format_var_name.split('.', 1)[0]
232 matches = path_parameters_by_segment.setdefault(first_segment, [])
233 matches.append(format_var_name)
234
235 return path_parameters_by_segment
236
237 def __validate_simple_subfield(self, parameter, field, segment_list,
238 segment_index=0):
239 """Verifies that a proposed subfield actually exists and is a simple field.
240
241 Here, simple means it is not a MessageField (nested).
242
243 Args:
244 parameter: String; the '.' delimited name of the current field being
245 considered. This is relative to some root.
246 field: An instance of a subclass of messages.Field. Corresponds to the
247 previous segment in the path (previous relative to _segment_index),
248 since this field should be a message field with the current segment
249 as a field in the message class.
250 segment_list: The full list of segments from the '.' delimited subfield
251 being validated.
252 segment_index: Integer; used to hold the position of current segment so
253 that segment_list can be passed as a reference instead of having to
254 copy using segment_list[1:] at each step.
255
256 Raises:
257 TypeError: If the final subfield (indicated by _segment_index relative
258 to the length of segment_list) is a MessageField.
259 TypeError: If at any stage the lookup at a segment fails, e.g if a.b
260 exists but a.b.c does not exist. This can happen either if a.b is not
261 a message field or if a.b.c is not a property on the message class from
262 a.b.
263 """
264 if segment_index >= len(segment_list):
265 # In this case, the field is the final one, so should be simple type
266 if isinstance(field, messages.MessageField):
267 field_class = field.__class__.__name__
268 raise TypeError('Can\'t use messages in path. Subfield %r was '
269 'included but is a %s.' % (parameter, field_class))
270 return
271
272 segment = segment_list[segment_index]
273 parameter += '.' + segment
274 try:
275 field = field.type.field_by_name(segment)
276 except (AttributeError, KeyError):
277 raise TypeError('Subfield %r from path does not exist.' % (parameter,))
278
279 self.__validate_simple_subfield(parameter, field, segment_list,
280 segment_index=segment_index + 1)
281
282 def __validate_path_parameters(self, field, path_parameters):
283 """Verifies that all path parameters correspond to an existing subfield.
284
285 Args:
286 field: An instance of a subclass of messages.Field. Should be the root
287 level property name in each path parameter in path_parameters. For
288 example, if the field is called 'foo', then each path parameter should
289 begin with 'foo.'.
290 path_parameters: A list of Strings representing URI parameter variables.
291
292 Raises:
293 TypeError: If one of the path parameters does not start with field.name.
294 """
295 for param in path_parameters:
296 segment_list = param.split('.')
297 if segment_list[0] != field.name:
298 raise TypeError('Subfield %r can\'t come from field %r.'
299 % (param, field.name))
300 self.__validate_simple_subfield(field.name, field, segment_list[1:])
301
302 def __parameter_default(self, field):
303 """Returns default value of field if it has one.
304
305 Args:
306 field: A simple field.
307
308 Returns:
309 The default value of the field, if any exists, with the exception of an
310 enum field, which will have its value cast to a string.
311 """
312 if field.default:
313 if isinstance(field, messages.EnumField):
314 return field.default.name
315 else:
316 return field.default
317
318 def __parameter_enum(self, param):
319 """Returns enum descriptor of a parameter if it is an enum.
320
321 An enum descriptor is a list of keys.
322
323 Args:
324 param: A simple field.
325
326 Returns:
327 The enum descriptor for the field, if it's an enum descriptor, else
328 returns None.
329 """
330 if isinstance(param, messages.EnumField):
331 return [enum_entry[0] for enum_entry in sorted(
332 param.type.to_dict().items(), key=lambda v: v[1])]
333
334 def __parameter_descriptor(self, param):
335 """Creates descriptor for a parameter.
336
337 Args:
338 param: The parameter to be described.
339
340 Returns:
341 Dictionary containing a descriptor for the parameter.
342 """
343 descriptor = {}
344
345 param_type, param_format = self.__field_to_parameter_type_and_format(param)
346
347 # Required
348 if param.required:
349 descriptor['required'] = True
350
351 # Type
352 descriptor['type'] = param_type
353
354 # Format (optional)
355 if param_format:
356 descriptor['format'] = param_format
357
358 # Default
359 default = self.__parameter_default(param)
360 if default is not None:
361 descriptor['default'] = default
362
363 # Repeated
364 if param.repeated:
365 descriptor['repeated'] = True
366
367 # Enum
368 # Note that enumDescriptions are not currently supported using the
369 # framework's annotations, so just insert blank strings.
370 enum_descriptor = self.__parameter_enum(param)
371 if enum_descriptor is not None:
372 descriptor['enum'] = enum_descriptor
373 descriptor['enumDescriptions'] = [''] * len(enum_descriptor)
374
375 return descriptor
376
377 def __add_parameter(self, param, path_parameters, params):
378 """Adds all parameters in a field to a method parameters descriptor.
379
380 Simple fields will only have one parameter, but a message field 'x' that
381 corresponds to a message class with fields 'y' and 'z' will result in
382 parameters 'x.y' and 'x.z', for example. The mapping from field to
383 parameters is mostly handled by __field_to_subfields.
384
385 Args:
386 param: Parameter to be added to the descriptor.
387 path_parameters: A list of parameters matched from a path for this field.
388 For example for the hypothetical 'x' from above if the path was
389 '/a/{x.z}/b/{other}' then this list would contain only the element
390 'x.z' since 'other' does not match to this field.
391 params: List of parameters. Each parameter in the field.
392 """
393 # If this is a simple field, just build the descriptor and append it.
394 # Otherwise, build a schema and assign it to this descriptor
395 descriptor = None
396 if not isinstance(param, messages.MessageField):
397 name = param.name
398 descriptor = self.__parameter_descriptor(param)
399 descriptor['location'] = 'path' if name in path_parameters else 'query'
400
401 if descriptor:
402 params[name] = descriptor
403 else:
404 for subfield_list in self.__field_to_subfields(param):
405 name = '.'.join(subfield.name for subfield in subfield_list)
406 descriptor = self.__parameter_descriptor(subfield_list[-1])
407 if name in path_parameters:
408 descriptor['required'] = True
409 descriptor['location'] = 'path'
410 else:
411 descriptor.pop('required', None)
412 descriptor['location'] = 'query'
413
414 if descriptor:
415 params[name] = descriptor
416
417
418 def __params_descriptor_without_container(self, message_type,
419 request_kind, path):
420 """Describe parameters of a method which does not use a ResourceContainer.
421
422 Makes sure that the path parameters are included in the message definition
423 and adds any required fields and URL query parameters.
424
425 This method is to preserve backwards compatibility and will be removed in
426 a future release.
427
428 Args:
429 message_type: messages.Message class, Message with parameters to describe.
430 request_kind: The type of request being made.
431 path: string, HTTP path to method.
432
433 Returns:
434 A list of dicts: Descriptors of the parameters
435 """
436 params = {}
437
438 path_parameter_dict = self.__get_path_parameters(path)
439 for field in sorted(message_type.all_fields(), key=lambda f: f.number):
440 matched_path_parameters = path_parameter_dict.get(field.name, [])
441 self.__validate_path_parameters(field, matched_path_parameters)
442 if matched_path_parameters or request_kind == self.__NO_BODY:
443 self.__add_parameter(field, matched_path_parameters, params)
444
445 return params
446
447 def __params_descriptor(self, message_type, request_kind, path, method_id,
448 request_params_class):
449 """Describe the parameters of a method.
450
451 If the message_type is not a ResourceContainer, will fall back to
452 __params_descriptor_without_container (which will eventually be deprecated).
453
454 If the message type is a ResourceContainer, then all path/query parameters
455 will come from the ResourceContainer. This method will also make sure all
456 path parameters are covered by the message fields.
457
458 Args:
459 message_type: messages.Message or ResourceContainer class, Message with
460 parameters to describe.
461 request_kind: The type of request being made.
462 path: string, HTTP path to method.
463 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
464 request_params_class: messages.Message, the original params message when
465 using a ResourceContainer. Otherwise, this should be null.
466
467 Returns:
468 A tuple (dict, list of string): Descriptor of the parameters, Order of the
469 parameters.
470 """
471 path_parameter_dict = self.__get_path_parameters(path)
472
473 if request_params_class is None:
474 if path_parameter_dict:
475 logging.warning('Method %s specifies path parameters but you are not '
476 'using a ResourceContainer. This will fail in future '
477 'releases; please switch to using ResourceContainer as '
478 'soon as possible.', method_id)
479 return self.__params_descriptor_without_container(
480 message_type, request_kind, path)
481
482 # From here, we can assume message_type is from a ResourceContainer.
483 message_type = request_params_class
484
485 params = {}
486
487 # Make sure all path parameters are covered.
488 for field_name, matched_path_parameters in path_parameter_dict.iteritems():
489 field = message_type.field_by_name(field_name)
490 self.__validate_path_parameters(field, matched_path_parameters)
491
492 # Add all fields, sort by field.number since we have parameterOrder.
493 for field in sorted(message_type.all_fields(), key=lambda f: f.number):
494 matched_path_parameters = path_parameter_dict.get(field.name, [])
495 self.__add_parameter(field, matched_path_parameters, params)
496
497 return params
498
499 def __params_order_descriptor(self, message_type, path):
500 """Describe the order of path parameters.
501
502 Args:
503 message_type: messages.Message class, Message with parameters to describe.
504 path: string, HTTP path to method.
505
506 Returns:
507 Descriptor list for the parameter order.
508 """
509 descriptor = []
510 path_parameter_dict = self.__get_path_parameters(path)
511
512 for field in sorted(message_type.all_fields(), key=lambda f: f.number):
513 matched_path_parameters = path_parameter_dict.get(field.name, [])
514 if not isinstance(field, messages.MessageField):
515 name = field.name
516 if name in matched_path_parameters:
517 descriptor.append(name)
518 else:
519 for subfield_list in self.__field_to_subfields(field):
520 name = '.'.join(subfield.name for subfield in subfield_list)
521 if name in matched_path_parameters:
522 descriptor.append(name)
523
524 return descriptor
525
526 def __schemas_descriptor(self):
527 """Describes the schemas section of the discovery document.
528
529 Returns:
530 Dictionary describing the schemas of the document.
531 """
532 # Filter out any keys that aren't 'properties', 'type', or 'id'
533 result = {}
534 for schema_key, schema_value in self.__parser.schemas().iteritems():
535 field_keys = schema_value.keys()
536 key_result = {}
537
538 # Some special processing for the properties value
539 if 'properties' in field_keys:
540 key_result['properties'] = schema_value['properties'].copy()
541 # Add in enumDescriptions for any enum properties and strip out
542 # the required tag for consistency with Java framework
543 for prop_key, prop_value in schema_value['properties'].iteritems():
544 if 'enum' in prop_value:
545 num_enums = len(prop_value['enum'])
546 key_result['properties'][prop_key]['enumDescriptions'] = (
547 [''] * num_enums)
548 key_result['properties'][prop_key].pop('required', None)
549
550 for key in ('type', 'id', 'description'):
551 if key in field_keys:
552 key_result[key] = schema_value[key]
553
554 if key_result:
555 result[schema_key] = key_result
556
557 # Add 'type': 'object' to all object properties
558 for schema_value in result.itervalues():
559 for field_value in schema_value.itervalues():
560 if isinstance(field_value, dict):
561 if '$ref' in field_value:
562 field_value['type'] = 'object'
563
564 return result
565
566 def __request_message_descriptor(self, request_kind, message_type, method_id,
567 request_body_class):
568 """Describes the parameters and body of the request.
569
570 Args:
571 request_kind: The type of request being made.
572 message_type: messages.Message or ResourceContainer class. The message to
573 describe.
574 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
575 request_body_class: messages.Message of the original body when using
576 a ResourceContainer. Otherwise, this should be null.
577
578 Returns:
579 Dictionary describing the request.
580
581 Raises:
582 ValueError: if the method path and request required fields do not match
583 """
584 if request_body_class:
585 message_type = request_body_class
586
587 if (request_kind != self.__NO_BODY and
588 message_type != message_types.VoidMessage()):
589 self.__request_schema[method_id] = self.__parser.add_message(
590 message_type.__class__)
591 return {
592 '$ref': self.__request_schema[method_id],
593 'parameterName': 'resource',
594 }
595
596 def __response_message_descriptor(self, message_type, method_id):
597 """Describes the response.
598
599 Args:
600 message_type: messages.Message class, The message to describe.
601 method_id: string, Unique method identifier (e.g. 'myapi.items.method')
602
603 Returns:
604 Dictionary describing the response.
605 """
606 if message_type != message_types.VoidMessage():
607 self.__parser.add_message(message_type.__class__)
608 self.__response_schema[method_id] = self.__parser.ref_for_message_type(
609 message_type.__class__)
610 return {'$ref': self.__response_schema[method_id]}
611 else:
612 return None
613
614 def __method_descriptor(self, service, method_info,
615 protorpc_method_info):
616 """Describes a method.
617
618 Args:
619 service: endpoints.Service, Implementation of the API as a service.
620 method_info: _MethodInfo, Configuration for the method.
621 protorpc_method_info: protorpc.remote._RemoteMethodInfo, ProtoRPC
622 description of the method.
623
624 Returns:
625 Dictionary describing the method.
626 """
627 descriptor = {}
628
629 request_message_type = (resource_container.ResourceContainer.
630 get_request_message(protorpc_method_info.remote))
631 request_kind = self.__get_request_kind(method_info)
632 remote_method = protorpc_method_info.remote
633
634 method_id = method_info.method_id(service.api_info)
635
636 path = method_info.get_path(service.api_info)
637
638 description = protorpc_method_info.remote.method.__doc__
639
640 descriptor['id'] = method_id
641 descriptor['path'] = path
642 descriptor['httpMethod'] = method_info.http_method
643
644 if description:
645 descriptor['description'] = description
646
647 descriptor['scopes'] = [
648 'https://www.googleapis.com/auth/userinfo.email'
649 ]
650
651 parameters = self.__params_descriptor(
652 request_message_type, request_kind, path, method_id,
653 method_info.request_params_class)
654 if parameters:
655 descriptor['parameters'] = parameters
656
657 if method_info.request_params_class:
658 parameter_order = self.__params_order_descriptor(
659 method_info.request_params_class, path)
660 else:
661 parameter_order = self.__params_order_descriptor(
662 request_message_type, path)
663 if parameter_order:
664 descriptor['parameterOrder'] = parameter_order
665
666 request_descriptor = self.__request_message_descriptor(
667 request_kind, request_message_type, method_id,
668 method_info.request_body_class)
669 if request_descriptor is not None:
670 descriptor['request'] = request_descriptor
671
672 response_descriptor = self.__response_message_descriptor(
673 remote_method.response_type(), method_info.method_id(service.api_info))
674 if response_descriptor is not None:
675 descriptor['response'] = response_descriptor
676
677 return descriptor
678
679 def __resource_descriptor(self, resource_path, methods):
680 """Describes a resource.
681
682 Args:
683 resource_path: string, the path of the resource (e.g., 'entries.items')
684 methods: list of tuples of type
685 (endpoints.Service, protorpc.remote._RemoteMethodInfo), the methods
686 that serve this resource.
687
688 Returns:
689 Dictionary describing the resource.
690 """
691 descriptor = {}
692 method_map = {}
693 sub_resource_index = collections.defaultdict(list)
694 sub_resource_map = {}
695
696 resource_path_tokens = resource_path.split('.')
697 for service, protorpc_meth_info in methods:
698 method_info = getattr(protorpc_meth_info, 'method_info', None)
699 path = method_info.get_path(service.api_info)
700 method_id = method_info.method_id(service.api_info)
701 canonical_method_id = self._get_canonical_method_id(method_id)
702
703 current_resource_path = self._get_resource_path(method_id)
704
705 # Sanity-check that this method belongs to the resource path
706 if (current_resource_path[:len(resource_path_tokens)] !=
707 resource_path_tokens):
708 raise api_exceptions.ToolError(
709 'Internal consistency error in resource path {0}'.format(
710 current_resource_path))
711
712 # Remove the portion of the current method's resource path that's already
713 # part of the resource path at this level.
714 effective_resource_path = current_resource_path[
715 len(resource_path_tokens):]
716
717 # If this method is part of a sub-resource, note it and skip it for now
718 if effective_resource_path:
719 sub_resource_name = effective_resource_path[0]
720 new_resource_path = '.'.join([resource_path, sub_resource_name])
721 sub_resource_index[new_resource_path].append(
722 (service, protorpc_meth_info))
723 else:
724 method_map[canonical_method_id] = self.__method_descriptor(
725 service, method_info, protorpc_meth_info)
726
727 # Process any sub-resources
728 for sub_resource, sub_resource_methods in sub_resource_index.items():
729 sub_resource_name = sub_resource.split('.')[-1]
730 sub_resource_map[sub_resource_name] = self.__resource_descriptor(
731 sub_resource, sub_resource_methods)
732
733 if method_map:
734 descriptor['methods'] = method_map
735
736 if sub_resource_map:
737 descriptor['resources'] = sub_resource_map
738
739 return descriptor
740
741 def __standard_parameters_descriptor(self):
742 return {
743 'alt': {
744 'type': 'string',
745 'description': 'Data format for the response.',
746 'default': 'json',
747 'enum': ['json'],
748 'enumDescriptions': [
749 'Responses with Content-Type of application/json'
750 ],
751 'location': 'query',
752 },
753 'fields': {
754 'type': 'string',
755 'description': 'Selector specifying which fields to include in a '
756 'partial response.',
757 'location': 'query',
758 },
759 'key': {
760 'type': 'string',
761 'description': 'API key. Your API key identifies your project and '
762 'provides you with API access, quota, and reports. '
763 'Required unless you provide an OAuth 2.0 token.',
764 'location': 'query',
765 },
766 'oauth_token': {
767 'type': 'string',
768 'description': 'OAuth 2.0 token for the current user.',
769 'location': 'query',
770 },
771 'prettyPrint': {
772 'type': 'boolean',
773 'description': 'Returns response with indentations and line '
774 'breaks.',
775 'default': 'true',
776 'location': 'query',
777 },
778 'quotaUser': {
779 'type': 'string',
780 'description': 'Available to use for quota purposes for '
781 'server-side applications. Can be any arbitrary '
782 'string assigned to a user, but should not exceed '
783 '40 characters. Overrides userIp if both are '
784 'provided.',
785 'location': 'query',
786 },
787 'userIp': {
788 'type': 'string',
789 'description': 'IP address of the site where the request '
790 'originates. Use this if you want to enforce '
791 'per-user limits.',
792 'location': 'query',
793 },
794 }
795
796 def __standard_auth_descriptor(self):
797 return {
798 'oauth2': {
799 'scopes': {
800 'https://www.googleapis.com/auth/userinfo.email': {
801 'description': 'View your email address'
802 }
803 }
804 }
805 }
806
807 def __get_merged_api_info(self, services):
808 """Builds a description of an API.
809
810 Args:
811 services: List of protorpc.remote.Service instances implementing an
812 api/version.
813
814 Returns:
815 The _ApiInfo object to use for the API that the given services implement.
816
817 Raises:
818 ApiConfigurationError: If there's something wrong with the API
819 configuration, such as a multiclass API decorated with different API
820 descriptors (see the docstring for api()).
821 """
822 merged_api_info = services[0].api_info
823
824 # Verify that, if there are multiple classes here, they're allowed to
825 # implement the same API.
826 for service in services[1:]:
827 if not merged_api_info.is_same_api(service.api_info):
828 raise api_exceptions.ApiConfigurationError(
829 _MULTICLASS_MISMATCH_ERROR_TEMPLATE % (service.api_info.name,
830 service.api_info.version))
831
832 return merged_api_info
833
834 def __discovery_doc_descriptor(self, services, hostname=None):
835 """Builds a discovery doc for an API.
836
837 Args:
838 services: List of protorpc.remote.Service instances implementing an
839 api/version.
840 hostname: string, Hostname of the API, to override the value set on the
841 current service. Defaults to None.
842
843 Returns:
844 A dictionary that can be deserialized into JSON in discovery doc format.
845
846 Raises:
847 ApiConfigurationError: If there's something wrong with the API
848 configuration, such as a multiclass API decorated with different API
849 descriptors (see the docstring for api()), or a repeated method
850 signature.
851 """
852 merged_api_info = self.__get_merged_api_info(services)
853 descriptor = self.get_descriptor_defaults(merged_api_info,
854 hostname=hostname)
855
856 description = merged_api_info.description
857 if not description and len(services) == 1:
858 description = services[0].__doc__
859 if description:
860 descriptor['description'] = description
861
862 descriptor['parameters'] = self.__standard_parameters_descriptor()
863 descriptor['auth'] = self.__standard_auth_descriptor()
864
865 method_map = {}
866 method_collision_tracker = {}
867 rest_collision_tracker = {}
868
869 resource_index = collections.defaultdict(list)
870 resource_map = {}
871
872 # For the first pass, only process top-level methods (that is, those methods
873 # that are unattached to a resource).
874 for service in services:
875 remote_methods = service.all_remote_methods()
876
877 for protorpc_meth_name, protorpc_meth_info in remote_methods.iteritems():
878 method_info = getattr(protorpc_meth_info, 'method_info', None)
879 # Skip methods that are not decorated with @method
880 if method_info is None:
881 continue
882 path = method_info.get_path(service.api_info)
883 method_id = method_info.method_id(service.api_info)
884 canonical_method_id = self._get_canonical_method_id(method_id)
885 resource_path = self._get_resource_path(method_id)
886
887 # Make sure the same method name isn't repeated.
888 if method_id in method_collision_tracker:
889 raise api_exceptions.ApiConfigurationError(
890 'Method %s used multiple times, in classes %s and %s' %
891 (method_id, method_collision_tracker[method_id],
892 service.__name__))
893 else:
894 method_collision_tracker[method_id] = service.__name__
895
896 # Make sure the same HTTP method & path aren't repeated.
897 rest_identifier = (method_info.http_method, path)
898 if rest_identifier in rest_collision_tracker:
899 raise api_exceptions.ApiConfigurationError(
900 '%s path "%s" used multiple times, in classes %s and %s' %
901 (method_info.http_method, path,
902 rest_collision_tracker[rest_identifier],
903 service.__name__))
904 else:
905 rest_collision_tracker[rest_identifier] = service.__name__
906
907 # If this method is part of a resource, note it and skip it for now
908 if resource_path:
909 resource_index[resource_path[0]].append((service, protorpc_meth_info))
910 else:
911 method_map[canonical_method_id] = self.__method_descriptor(
912 service, method_info, protorpc_meth_info)
913
914 # Do another pass for methods attached to resources
915 for resource, resource_methods in resource_index.items():
916 resource_map[resource] = self.__resource_descriptor(resource,
917 resource_methods)
918
919 if method_map:
920 descriptor['methods'] = method_map
921
922 if resource_map:
923 descriptor['resources'] = resource_map
924
925 # Add schemas, if any
926 schemas = self.__schemas_descriptor()
927 if schemas:
928 descriptor['schemas'] = schemas
929
930 return descriptor
931
932 def get_descriptor_defaults(self, api_info, hostname=None):
933 """Gets a default configuration for a service.
934
935 Args:
936 api_info: _ApiInfo object for this service.
937 hostname: string, Hostname of the API, to override the value set on the
938 current service. Defaults to None.
939
940 Returns:
941 A dictionary with the default configuration.
942 """
943 hostname = (hostname or util.get_app_hostname() or
944 api_info.hostname)
945 protocol = 'http' if ((hostname and hostname.startswith('localhost')) or
946 util.is_running_on_devserver()) else 'https'
947 full_base_path = '{0}{1}/{2}/'.format(api_info.base_path,
948 api_info.name,
949 api_info.version)
950 base_url = '{0}://{1}{2}'.format(protocol, hostname, full_base_path)
951 root_url = '{0}://{1}{2}'.format(protocol, hostname, api_info.base_path)
952 defaults = {
953 'kind': 'discovery#restDescription',
954 'discoveryVersion': 'v1',
955 'id': '{0}:{1}'.format(api_info.name, api_info.version),
956 'name': api_info.name,
957 'version': api_info.version,
958 'icons': {
959 'x16': 'http://www.google.com/images/icons/product/search-16.gif',
960 'x32': 'http://www.google.com/images/icons/product/search-32.gif'
961 },
962 'protocol': 'rest',
963 'servicePath': '{0}/{1}/'.format(api_info.name, api_info.version),
964 'batchPath': 'batch',
965 'basePath': full_base_path,
966 'rootUrl': root_url,
967 'baseUrl': base_url,
968 }
969
970 return defaults
971
972 def get_discovery_doc(self, services, hostname=None):
973 """JSON dict description of a protorpc.remote.Service in discovery format.
974
975 Args:
976 services: Either a single protorpc.remote.Service or a list of them
977 that implements an api/version.
978 hostname: string, Hostname of the API, to override the value set on the
979 current service. Defaults to None.
980
981 Returns:
982 dict, The discovery document as a JSON dict.
983 """
984
985 if not isinstance(services, (tuple, list)):
986 services = [services]
987
988 # The type of a class that inherits from remote.Service is actually
989 # remote._ServiceClass, thanks to metaclass strangeness.
990 # pylint: disable=protected-access
991 util.check_list_type(services, remote._ServiceClass, 'services',
992 allow_none=False)
993
994 return self.__discovery_doc_descriptor(services, hostname=hostname)
995
996 def pretty_print_config_to_json(self, services, hostname=None):
997 """JSON string description of a protorpc.remote.Service in a discovery doc.
998
999 Args:
1000 services: Either a single protorpc.remote.Service or a list of them
1001 that implements an api/version.
1002 hostname: string, Hostname of the API, to override the value set on the
1003 current service. Defaults to None.
1004
1005 Returns:
1006 string, The discovery doc descriptor document as a JSON string.
1007 """
1008 descriptor = self.get_discovery_doc(services, hostname)
1009 return json.dumps(descriptor, sort_keys=True, indent=2,
1010 separators=(',', ': '))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698