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

Commit d6108fa

Browse files
authored
Allow opt-in to new api name restrictions. (#144)
1 parent 74ee5fb commit d6108fa

5 files changed

Lines changed: 63 additions & 64 deletions

File tree

endpoints/_endpointscfg_impl.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def _WriteFile(output_path, name, content):
151151

152152

153153
def GenApiConfig(service_class_names, config_string_generator=None,
154-
hostname=None, application_path=None):
154+
hostname=None, application_path=None, **additional_kwargs):
155155
"""Write an API configuration for endpoints annotated ProtoRPC services.
156156
157157
Args:
@@ -211,7 +211,7 @@ def GenApiConfig(service_class_names, config_string_generator=None,
211211
# Map each API by name-version.
212212
service_map['%s-%s' % api_info] = (
213213
config_string_generator.pretty_print_config_to_json(
214-
services, hostname=hostname))
214+
services, hostname=hostname, **additional_kwargs))
215215

216216
return service_map
217217

@@ -311,7 +311,7 @@ def _GenDiscoveryDoc(service_class_names, doc_format,
311311

312312

313313
def _GenOpenApiSpec(service_class_names, output_path, hostname=None,
314-
application_path=None):
314+
application_path=None, x_google_api_name=False):
315315
"""Write discovery documents generated from a cloud service to file.
316316
317317
Args:
@@ -329,7 +329,8 @@ def _GenOpenApiSpec(service_class_names, output_path, hostname=None,
329329
service_configs = GenApiConfig(
330330
service_class_names, hostname=hostname,
331331
config_string_generator=openapi_generator.OpenApiGenerator(),
332-
application_path=application_path)
332+
application_path=application_path,
333+
x_google_api_name=x_google_api_name)
333334
for api_name_version, config in service_configs.iteritems():
334335
openapi_name = api_name_version.replace('-', '') + 'openapi.json'
335336
output_files.append(_WriteFile(output_path, openapi_name, config))
@@ -484,7 +485,8 @@ def _GenOpenApiSpecCallback(args, openapi_func=_GenOpenApiSpec):
484485
"""
485486
openapi_paths = openapi_func(args.service, args.output,
486487
hostname=args.hostname,
487-
application_path=args.application)
488+
application_path=args.application,
489+
x_google_api_name=args.x_google_api_name)
488490
for openapi_path in openapi_paths:
489491
print 'OpenAPI spec written to %s' % openapi_path
490492

@@ -572,6 +574,8 @@ def AddStandardOptions(parser, *args):
572574
get_openapi_spec.set_defaults(callback=_GenOpenApiSpecCallback)
573575
AddStandardOptions(get_openapi_spec, 'application', 'hostname', 'output',
574576
'service')
577+
get_openapi_spec.add_argument('--x-google-api-name', action='store_true',
578+
help="Add the 'x-google-api-name' field to the generated spec")
575579

576580
# Create an alias for get_openapi_spec called get_swagger_spec to support
577581
# the old-style naming. This won't be a visible command, but it will still

endpoints/api_config.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -258,17 +258,6 @@ def _CheckLimitDefinitions(limit_definitions):
258258
_CheckType(ld.default_limit, int, 'limit_definition.default_limit')
259259

260260

261-
_VALID_API_NAME = re.compile('^[a-z][a-z0-9]{0,39}$')
262-
263-
264-
def _CheckApiName(name):
265-
valid = (_VALID_API_NAME.match(name) is not None)
266-
if not valid:
267-
raise api_exceptions.InvalidApiNameException(
268-
'The API name must match the regular expression {}'.format(
269-
_VALID_API_NAME.pattern[1:-1]))
270-
271-
272261
# pylint: disable=g-bad-name
273262
class _ApiInfo(object):
274263
"""Configurable attributes of an API.
@@ -592,7 +581,6 @@ def __init__(self, name, version, description=None, hostname=None,
592581
limit_definitions: list of LimitDefinition tuples used in this API.
593582
"""
594583
_CheckType(name, basestring, 'name', allow_none=False)
595-
_CheckApiName(name)
596584
_CheckType(version, basestring, 'version', allow_none=False)
597585
_CheckType(description, basestring, 'description')
598586
_CheckType(hostname, basestring, 'hostname')

endpoints/openapi_generator.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,18 @@
4545
_DEFAULT_SECURITY_DEFINITION = 'google_id_token'
4646

4747

48+
_VALID_API_NAME = re.compile('^[a-z][a-z0-9]{0,39}$')
49+
50+
51+
def _validate_api_name(name):
52+
valid = (_VALID_API_NAME.match(name) is not None)
53+
if not valid:
54+
raise api_exceptions.InvalidApiNameException(
55+
'The API name must match the regular expression {}'.format(
56+
_VALID_API_NAME.pattern[1:-1]))
57+
return name
58+
59+
4860
class OpenApiGenerator(object):
4961
"""Generates an OpenAPI spec from a ProtoRPC service.
5062
@@ -845,7 +857,7 @@ def __get_merged_api_info(self, services):
845857

846858
return merged_api_info
847859

848-
def __api_openapi_descriptor(self, services, hostname=None):
860+
def __api_openapi_descriptor(self, services, hostname=None, x_google_api_name=False):
849861
"""Builds an OpenAPI description of an API.
850862
851863
Args:
@@ -866,7 +878,8 @@ def __api_openapi_descriptor(self, services, hostname=None):
866878
"""
867879
merged_api_info = self.__get_merged_api_info(services)
868880
descriptor = self.get_descriptor_defaults(merged_api_info,
869-
hostname=hostname)
881+
hostname=hostname,
882+
x_google_api_name=x_google_api_name)
870883

871884
description = merged_api_info.description
872885
if not description and len(services) == 1:
@@ -956,7 +969,7 @@ def __api_openapi_descriptor(self, services, hostname=None):
956969

957970
return descriptor
958971

959-
def get_descriptor_defaults(self, api_info, hostname=None):
972+
def get_descriptor_defaults(self, api_info, hostname=None, x_google_api_name=False):
960973
"""Gets a default configuration for a service.
961974
962975
Args:
@@ -980,17 +993,19 @@ def get_descriptor_defaults(self, api_info, hostname=None):
980993
'version': api_info.api_version,
981994
'title': api_info.name
982995
},
983-
'x-google-api-name': api_info.name,
984996
'host': hostname,
985997
'consumes': ['application/json'],
986998
'produces': ['application/json'],
987999
'schemes': [protocol],
9881000
'basePath': base_path,
9891001
}
9901002

1003+
if x_google_api_name:
1004+
defaults['x-google-api-name'] = _validate_api_name(api_info.name)
1005+
9911006
return defaults
9921007

993-
def get_openapi_dict(self, services, hostname=None):
1008+
def get_openapi_dict(self, services, hostname=None, x_google_api_name=False):
9941009
"""JSON dict description of a protorpc.remote.Service in OpenAPI format.
9951010
9961011
Args:
@@ -1012,9 +1027,9 @@ def get_openapi_dict(self, services, hostname=None):
10121027
util.check_list_type(services, remote._ServiceClass, 'services',
10131028
allow_none=False)
10141029

1015-
return self.__api_openapi_descriptor(services, hostname=hostname)
1030+
return self.__api_openapi_descriptor(services, hostname=hostname, x_google_api_name=x_google_api_name)
10161031

1017-
def pretty_print_config_to_json(self, services, hostname=None):
1032+
def pretty_print_config_to_json(self, services, hostname=None, x_google_api_name=False):
10181033
"""JSON string description of a protorpc.remote.Service in OpenAPI format.
10191034
10201035
Args:
@@ -1026,7 +1041,7 @@ def pretty_print_config_to_json(self, services, hostname=None):
10261041
Returns:
10271042
string, The OpenAPI descriptor document as a JSON string.
10281043
"""
1029-
descriptor = self.get_openapi_dict(services, hostname)
1044+
descriptor = self.get_openapi_dict(services, hostname, x_google_api_name=x_google_api_name)
10301045
return json.dumps(descriptor, sort_keys=True, indent=2,
10311046
separators=(',', ': '))
10321047

endpoints/test/api_config_test.py

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import itertools
1818
import json
1919
import unittest
20-
import pytest
2120

2221
import endpoints.api_config as api_config
2322
from endpoints.api_config import ApiConfigGenerator
@@ -2110,28 +2109,6 @@ class MyDecoratedService3(remote.Service):
21102109
self.assertEqual([MyDecoratedService1, MyDecoratedService2,
21112110
MyDecoratedService3], my_api.get_api_classes())
21122111

2113-
def testApiNameRestrictions(self):
2114-
2115-
@api_config.api(name='coolservice', version='vX')
2116-
class MyDecoratedService(remote.Service):
2117-
"""Describes MyDecoratedService."""
2118-
pass
2119-
2120-
api_info = MyDecoratedService.api_info
2121-
assert 'coolservice' == api_info.name
2122-
2123-
with pytest.raises(api_exceptions.InvalidApiNameException):
2124-
@api_config.api('CoolService2', 'v2')
2125-
class MyDecoratedService(remote.Service):
2126-
"""Describes MyDecoratedService."""
2127-
pass
2128-
2129-
with pytest.raises(api_exceptions.InvalidApiNameException):
2130-
@api_config.api('c' + 'o'*40 + 'l', 'v2')
2131-
class MyDecoratedService(remote.Service):
2132-
"""Describes MyDecoratedService."""
2133-
pass
2134-
21352112
class MethodDecoratorTest(unittest.TestCase):
21362113

21372114
def testMethodId(self):

endpoints/test/openapi_generator_test.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
import json
1818
import unittest
19+
import pytest
1920

2021
import endpoints.api_config as api_config
2122

2223
from protorpc import message_types
2324
from protorpc import messages
2425
from protorpc import remote
2526

27+
import endpoints.api_exceptions as api_exceptions
2628
import endpoints.resource_container as resource_container
2729
import endpoints.openapi_generator as openapi_generator
2830
import test_util
@@ -154,7 +156,6 @@ def entries_post_audiences(self, unused_request):
154156
'description': 'Describes MyService.',
155157
'version': 'v1',
156158
},
157-
'x-google-api-name': 'root',
158159
'host': 'example.appspot.com',
159160
'consumes': ['application/json'],
160161
'produces': ['application/json'],
@@ -481,7 +482,6 @@ def items_put_container(self, unused_request):
481482
'description': 'Describes MyService.',
482483
'version': 'v1',
483484
},
484-
'x-google-api-name': 'root',
485485
'host': 'example.appspot.com',
486486
'consumes': ['application/json'],
487487
'produces': ['application/json'],
@@ -1033,7 +1033,6 @@ def get_airport_2(self, request):
10331033
'title': 'iata',
10341034
'version': 'v1',
10351035
},
1036-
'x-google-api-name': 'iata',
10371036
'host': None,
10381037
'consumes': ['application/json'],
10391038
'produces': ['application/json'],
@@ -1127,7 +1126,6 @@ def noop_get(self, unused_request):
11271126
'description': 'Describes MyService.',
11281127
'version': 'v1',
11291128
},
1130-
'x-google-api-name': 'root',
11311129
'host': 'localhost:8080',
11321130
'consumes': ['application/json'],
11331131
'produces': ['application/json'],
@@ -1191,7 +1189,6 @@ def noop_get(self, unused_request):
11911189
'description': 'Describes MyService.',
11921190
'version': 'v1',
11931191
},
1194-
'x-google-api-name': 'root',
11951192
'host': 'example.appspot.com',
11961193
'consumes': ['application/json'],
11971194
'produces': ['application/json'],
@@ -1292,7 +1289,6 @@ def override_get(self, unused_request):
12921289
'description': 'Describes MyService.',
12931290
'version': 'v1',
12941291
},
1295-
'x-google-api-name': 'root',
12961292
'host': 'example.appspot.com',
12971293
'consumes': ['application/json'],
12981294
'produces': ['application/json'],
@@ -1365,7 +1361,6 @@ def noop_get(self, unused_request):
13651361
'description': 'Describes MyService.',
13661362
'version': '1.3.4',
13671363
},
1368-
'x-google-api-name': 'root',
13691364
'host': 'example.appspot.com',
13701365
'consumes': ['application/json'],
13711366
'produces': ['application/json'],
@@ -1418,7 +1413,6 @@ def noop_get(self, unused_request):
14181413
'description': 'Describes MyService.',
14191414
'version': 'v1',
14201415
},
1421-
'x-google-api-name': 'root',
14221416
'host': 'example.appspot.com',
14231417
'consumes': ['application/json'],
14241418
'produces': ['application/json'],
@@ -1471,7 +1465,6 @@ def noop_get(self, unused_request):
14711465
'description': 'Describes MyService.',
14721466
'version': 'v1',
14731467
},
1474-
'x-google-api-name': 'root',
14751468
'host': 'example.appspot.com',
14761469
'consumes': ['application/json'],
14771470
'produces': ['application/json'],
@@ -1524,7 +1517,6 @@ def toplevel(self, unused_request):
15241517
'description': 'Testing repeated params',
15251518
'version': 'v1',
15261519
},
1527-
'x-google-api-name': 'root',
15281520
'host': 'example.appspot.com',
15291521
'consumes': ['application/json'],
15301522
'produces': ['application/json'],
@@ -1606,7 +1598,6 @@ def toplevel(self, unused_request):
16061598
'description': 'Testing repeated simple field params',
16071599
'version': 'v1',
16081600
},
1609-
'x-google-api-name': 'root',
16101601
'host': 'example.appspot.com',
16111602
'consumes': ['application/json'],
16121603
'produces': ['application/json'],
@@ -1682,7 +1673,6 @@ def toplevel(self, unused_request):
16821673
'description': 'Testing repeated Message params',
16831674
'version': 'v1',
16841675
},
1685-
'x-google-api-name': 'root',
16861676
'host': 'example.appspot.com',
16871677
'consumes': ['application/json'],
16881678
'produces': ['application/json'],
@@ -1749,6 +1739,35 @@ def toplevel(self, unused_request):
17491739

17501740
test_util.AssertDictEqual(expected_openapi, api, self)
17511741

1742+
def testApiNameRestrictions(self):
1743+
@api_config.api(name='coolservice', version='vX')
1744+
class MyDecoratedService(remote.Service):
1745+
pass
1746+
1747+
api = json.loads(self.generator.pretty_print_config_to_json(MyDecoratedService))
1748+
assert 'x-google-api-name' not in api
1749+
api = json.loads(self.generator.pretty_print_config_to_json(MyDecoratedService, x_google_api_name=True))
1750+
assert 'x-google-api-name' in api
1751+
assert 'coolservice' == api['x-google-api-name']
1752+
1753+
@api_config.api('CoolService2', 'v2')
1754+
class MyDecoratedService(remote.Service):
1755+
pass
1756+
1757+
api = json.loads(self.generator.pretty_print_config_to_json(MyDecoratedService))
1758+
assert 'x-google-api-name' not in api
1759+
with pytest.raises(api_exceptions.InvalidApiNameException):
1760+
self.generator.pretty_print_config_to_json(MyDecoratedService, x_google_api_name=True)
1761+
1762+
@api_config.api('c' + 'o'*40 + 'l', 'v2')
1763+
class MyDecoratedService(remote.Service):
1764+
pass
1765+
1766+
api = json.loads(self.generator.pretty_print_config_to_json(MyDecoratedService))
1767+
assert 'x-google-api-name' not in api
1768+
with pytest.raises(api_exceptions.InvalidApiNameException):
1769+
self.generator.pretty_print_config_to_json(MyDecoratedService, x_google_api_name=True)
1770+
17521771

17531772
class DevServerOpenApiGeneratorTest(BaseOpenApiGeneratorTest,
17541773
test_util.DevServerTest):
@@ -1780,7 +1799,6 @@ def noop_get(self, unused_request):
17801799
'description': 'Describes MyService.',
17811800
'version': 'v1',
17821801
},
1783-
'x-google-api-name': 'root',
17841802
'host': 'example.appspot.com',
17851803
'consumes': ['application/json'],
17861804
'produces': ['application/json'],
@@ -1846,7 +1864,6 @@ def entries_post_audience(self, unused_request):
18461864
'description': 'Describes MyService.',
18471865
'version': 'v1',
18481866
},
1849-
'x-google-api-name': 'root',
18501867
'host': 'example.appspot.com',
18511868
'consumes': ['application/json'],
18521869
'produces': ['application/json'],
@@ -2044,7 +2061,6 @@ def entries_post(self, unused_request):
20442061
'description': 'Describes MyService.',
20452062
'version': 'v1',
20462063
},
2047-
'x-google-api-name': 'root',
20482064
'host': 'example.appspot.com',
20492065
'consumes': ['application/json'],
20502066
'produces': ['application/json'],
@@ -2131,7 +2147,6 @@ def entries_post(self, unused_request):
21312147
'description': 'Describes MyService.',
21322148
'version': 'v1',
21332149
},
2134-
'x-google-api-name': 'root',
21352150
'host': 'example.appspot.com',
21362151
'consumes': ['application/json'],
21372152
'produces': ['application/json'],

0 commit comments

Comments
 (0)