Skip to content
This repository was archived by the owner on Apr 19, 2026. It is now read-only.

Commit 40df3fb

Browse files
authored
Several discovery document fixes (#155)
* Include required query parameters in parameterOrder in discovery * Update icon paths in discovery * Include description, title, doclink, and canonical name in discovery * Port some discovery document tests from Java framework * Stringify default values in discovery doc
1 parent b6f88d4 commit 40df3fb

7 files changed

Lines changed: 767 additions & 14 deletions

File tree

endpoints/discovery_generator.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def __parameter_default(self, field):
324324
if isinstance(field, messages.EnumField):
325325
return field.default.name
326326
else:
327-
return field.default
327+
return str(field.default)
328328

329329
def __parameter_enum(self, param):
330330
"""Returns enum descriptor of a parameter if it is an enum.
@@ -508,32 +508,38 @@ def __params_descriptor(self, message_type, request_kind, path, method_id,
508508

509509
return params
510510

511-
def __params_order_descriptor(self, message_type, path):
511+
def __params_order_descriptor(self, message_type, path, is_params_class=False):
512512
"""Describe the order of path parameters.
513513
514514
Args:
515515
message_type: messages.Message class, Message with parameters to describe.
516516
path: string, HTTP path to method.
517+
is_params_class: boolean, Whether the message represents URL parameters.
517518
518519
Returns:
519520
Descriptor list for the parameter order.
520521
"""
521-
descriptor = []
522+
path_params = []
523+
query_params = []
522524
path_parameter_dict = self.__get_path_parameters(path)
523525

524526
for field in sorted(message_type.all_fields(), key=lambda f: f.number):
525527
matched_path_parameters = path_parameter_dict.get(field.name, [])
526528
if not isinstance(field, messages.MessageField):
527529
name = field.name
528530
if name in matched_path_parameters:
529-
descriptor.append(name)
531+
path_params.append(name)
532+
elif is_params_class and field.required:
533+
query_params.append(name)
530534
else:
531535
for subfield_list in self.__field_to_subfields(field):
532536
name = '.'.join(subfield.name for subfield in subfield_list)
533537
if name in matched_path_parameters:
534-
descriptor.append(name)
538+
path_params.append(name)
539+
elif is_params_class and field.required:
540+
query_params.append(name)
535541

536-
return descriptor
542+
return path_params + sorted(query_params)
537543

538544
def __schemas_descriptor(self):
539545
"""Describes the schemas section of the discovery document.
@@ -557,6 +563,9 @@ def __schemas_descriptor(self):
557563
num_enums = len(prop_value['enum'])
558564
key_result['properties'][prop_key]['enumDescriptions'] = (
559565
[''] * num_enums)
566+
elif 'default' in prop_value:
567+
# stringify default values
568+
prop_value['default'] = str(prop_value['default'])
560569
key_result['properties'][prop_key].pop('required', None)
561570

562571
for key in ('type', 'id', 'description'):
@@ -668,10 +677,10 @@ def __method_descriptor(self, service, method_info,
668677

669678
if method_info.request_params_class:
670679
parameter_order = self.__params_order_descriptor(
671-
method_info.request_params_class, path)
680+
method_info.request_params_class, path, is_params_class=True)
672681
else:
673682
parameter_order = self.__params_order_descriptor(
674-
request_message_type, path)
683+
request_message_type, path, is_params_class=False)
675684
if parameter_order:
676685
descriptor['parameterOrder'] = parameter_order
677686

@@ -972,16 +981,25 @@ def get_descriptor_defaults(self, api_info, hostname=None):
972981
'name': api_info.name,
973982
'version': api_info.api_version,
974983
'icons': {
975-
'x16': 'http://www.google.com/images/icons/product/search-16.gif',
976-
'x32': 'http://www.google.com/images/icons/product/search-32.gif'
984+
'x16': 'https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png',
985+
'x32': 'https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png'
977986
},
978987
'protocol': 'rest',
979988
'servicePath': '{0}/{1}/'.format(api_info.name, api_info.path_version),
980989
'batchPath': 'batch',
981990
'basePath': full_base_path,
982991
'rootUrl': root_url,
983992
'baseUrl': base_url,
993+
'description': 'This is an API',
984994
}
995+
if api_info.description:
996+
defaults['description'] = api_info.description
997+
if api_info.title:
998+
defaults['title'] = api_info.title
999+
if api_info.documentation:
1000+
defaults['documentationLink'] = api_info.documentation
1001+
if api_info.canonical_name:
1002+
defaults['canonicalName'] = api_info.canonical_name
9851003

9861004
return defaults
9871005

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Copyright 2018 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+
"""Test various discovery docs"""
16+
17+
import json
18+
import os.path
19+
20+
import pytest
21+
import webtest
22+
import urllib
23+
24+
import endpoints
25+
import endpoints.discovery_generator as discovery_generator
26+
from protorpc import message_types
27+
from protorpc import messages
28+
from protorpc import remote
29+
30+
def make_collection(cls):
31+
return type(
32+
'Collection_{}'.format(cls.__name__),
33+
(messages.Message,),
34+
{
35+
'items': messages.MessageField(cls, 1, repeated=True),
36+
'nextPageToken': messages.StringField(2)
37+
})
38+
39+
def load_expected_document(filename):
40+
try:
41+
pwd = os.path.dirname(os.path.realpath(__file__))
42+
test_file = os.path.join(pwd, 'testdata', 'discovery', filename)
43+
with open(test_file) as f:
44+
return json.loads(f.read())
45+
except IOError as e:
46+
print 'Could not find expected output file ' + test_file
47+
raise e
48+
49+
50+
class Foo(messages.Message):
51+
name = messages.StringField(1)
52+
value = messages.IntegerField(2, variant=messages.Variant.INT32)
53+
54+
FooCollection = make_collection(Foo)
55+
FooResource = endpoints.ResourceContainer(
56+
Foo,
57+
id=messages.StringField(1, required=True),
58+
)
59+
FooIdResource = endpoints.ResourceContainer(
60+
message_types.VoidMessage,
61+
id=messages.StringField(1, required=True),
62+
)
63+
FooNResource = endpoints.ResourceContainer(
64+
message_types.VoidMessage,
65+
n = messages.IntegerField(1, required=True, variant=messages.Variant.INT32),
66+
)
67+
68+
@endpoints.api(
69+
name='foo', version='v1', audiences=['audiences'],
70+
title='The Foo API', description='Just Foo Things',
71+
documentation='https://example.com', canonical_name='CanonicalName')
72+
class FooEndpoint(remote.Service):
73+
@endpoints.method(FooResource, Foo, name='foo.create', path='foos/{id}', http_method='PUT')
74+
def createFoo(self, request):
75+
pass
76+
@endpoints.method(FooIdResource, Foo, name='foo.get', path='foos/{id}', http_method='GET')
77+
def getFoo(self, request):
78+
pass
79+
@endpoints.method(FooResource, Foo, name='foo.update', path='foos/{id}', http_method='POST')
80+
def updateFoo(self, request):
81+
pass
82+
@endpoints.method(FooIdResource, Foo, name='foo.delete', path='foos/{id}', http_method='DELETE')
83+
def deleteFoo(self, request):
84+
pass
85+
@endpoints.method(FooNResource, FooCollection, name='foo.list', path='foos', http_method='GET')
86+
def listFoos(self, request):
87+
pass
88+
@endpoints.method(message_types.VoidMessage, FooCollection, name='toplevel', path='foos', http_method='POST')
89+
def toplevel(self, request):
90+
pass
91+
92+
93+
class Bar(messages.Message):
94+
name = messages.StringField(1, default='Jimothy')
95+
value = messages.IntegerField(2, default=42, variant=messages.Variant.INT32)
96+
active = messages.BooleanField(3, default=True)
97+
98+
BarCollection = make_collection(Bar)
99+
BarResource = endpoints.ResourceContainer(
100+
Bar,
101+
id=messages.StringField(1, required=True),
102+
)
103+
BarIdResource = endpoints.ResourceContainer(
104+
message_types.VoidMessage,
105+
id=messages.StringField(1, required=True),
106+
)
107+
BarNResource = endpoints.ResourceContainer(
108+
message_types.VoidMessage,
109+
n = messages.IntegerField(1, required=True, variant=messages.Variant.INT32),
110+
)
111+
112+
@endpoints.api(name='bar', version='v1')
113+
class BarEndpoint(remote.Service):
114+
@endpoints.method(BarResource, Bar, name='bar.create', path='bars/{id}', http_method='PUT')
115+
def createBar(self, request):
116+
pass
117+
@endpoints.method(BarIdResource, Bar, name='bar.get', path='bars/{id}', http_method='GET')
118+
def getBar(self, request):
119+
pass
120+
@endpoints.method(BarResource, Bar, name='bar.update', path='bars/{id}', http_method='POST')
121+
def updateBar(self, request):
122+
pass
123+
@endpoints.method(BarIdResource, Bar, name='bar.delete', path='bars/{id}', http_method='DELETE')
124+
def deleteBar(self, request):
125+
pass
126+
@endpoints.method(BarNResource, BarCollection, name='bar.list', path='bars', http_method='GET')
127+
def listBars(self, request):
128+
pass
129+
130+
131+
@endpoints.api(name='multipleparam', version='v1')
132+
class MultipleParameterEndpoint(remote.Service):
133+
@endpoints.method(endpoints.ResourceContainer(
134+
message_types.VoidMessage,
135+
parent = messages.StringField(1, required=True),
136+
query = messages.StringField(2, required=False),
137+
child = messages.StringField(3, required=True),
138+
queryb = messages.StringField(4, required=True),
139+
querya = messages.StringField(5, required=True),
140+
), message_types.VoidMessage, name='param', path='param/{parent}/{child}')
141+
def param(self, request):
142+
pass
143+
144+
@pytest.mark.parametrize('endpoint, json_filename', [
145+
(FooEndpoint, 'foo_endpoint.json'),
146+
(BarEndpoint, 'bar_endpoint.json'),
147+
(MultipleParameterEndpoint, 'multiple_parameter_endpoint.json'),
148+
])
149+
def test_discovery(endpoint, json_filename):
150+
generator = discovery_generator.DiscoveryGenerator()
151+
# JSON roundtrip so we get consistent string types
152+
actual = json.loads(generator.pretty_print_config_to_json(
153+
[endpoint], hostname='discovery-test.appspot.com'))
154+
expected = load_expected_document(json_filename)
155+
assert actual == expected

endpoints/test/testdata/discovery/allfields.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
"version":"v1",
77
"description":"This is an API",
88
"icons":{
9-
"x16":"http://www.google.com/images/icons/product/search-16.gif",
10-
"x32":"http://www.google.com/images/icons/product/search-32.gif"
9+
"x16": "https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png",
10+
"x32": "https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png"
1111
},
1212
"protocol":"rest",
1313
"baseUrl":"https://example.appspot.com/_ah/api/root/v1/",

0 commit comments

Comments
 (0)