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

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

Powered by Google App Engine
This is Rietveld 408576698