diff --git a/endpoints/api_config.py b/endpoints/api_config.py index b4392df..11293dc 100644 --- a/endpoints/api_config.py +++ b/endpoints/api_config.py @@ -258,6 +258,17 @@ def _CheckLimitDefinitions(limit_definitions): _CheckType(ld.default_limit, int, 'limit_definition.default_limit') +_VALID_API_NAME = re.compile('^[a-z][a-z0-9]{0,39}$') + + +def _CheckApiName(name): + valid = (_VALID_API_NAME.match(name) is not None) + if not valid: + raise api_exceptions.InvalidApiNameException( + 'The API name must match the regular expression {}'.format( + _VALID_API_NAME.pattern[1:-1])) + + # pylint: disable=g-bad-name class _ApiInfo(object): """Configurable attributes of an API. @@ -581,6 +592,7 @@ def __init__(self, name, version, description=None, hostname=None, limit_definitions: list of LimitDefinition tuples used in this API. """ _CheckType(name, basestring, 'name', allow_none=False) + _CheckApiName(name) _CheckType(version, basestring, 'version', allow_none=False) _CheckType(description, basestring, 'description') _CheckType(hostname, basestring, 'hostname') diff --git a/endpoints/api_exceptions.py b/endpoints/api_exceptions.py index 023f86e..41cf149 100644 --- a/endpoints/api_exceptions.py +++ b/endpoints/api_exceptions.py @@ -86,5 +86,9 @@ class InvalidLimitDefinitionException(Exception): """Exception thrown if there's an invalid rate limit definition.""" +class InvalidApiNameException(Exception): + """Exception thrown if the api name does not match the required character set.""" + + class ToolError(Exception): """Exception thrown if there's a general error in the endpointscfg.py tool.""" diff --git a/endpoints/openapi_generator.py b/endpoints/openapi_generator.py index f056969..18dea16 100644 --- a/endpoints/openapi_generator.py +++ b/endpoints/openapi_generator.py @@ -980,6 +980,7 @@ def get_descriptor_defaults(self, api_info, hostname=None): 'version': api_info.api_version, 'title': api_info.name }, + 'x-google-api-name': api_info.name, 'host': hostname, 'consumes': ['application/json'], 'produces': ['application/json'], diff --git a/endpoints/test/api_config_test.py b/endpoints/test/api_config_test.py index be00838..4c2179a 100644 --- a/endpoints/test/api_config_test.py +++ b/endpoints/test/api_config_test.py @@ -17,6 +17,7 @@ import itertools import json import unittest +import pytest import endpoints.api_config as api_config from endpoints.api_config import ApiConfigGenerator @@ -1974,7 +1975,7 @@ class ApiDecoratorTest(unittest.TestCase): def testApiInfoPopulated(self): - @api_config.api(name='CoolService', version='vX', + @api_config.api(name='coolservice', version='vX', description='My Cool Service', hostname='myhost.com', canonical_name='Cool Service Name', namespace=api_config.Namespace('domain', 'name', 'path')) @@ -1983,7 +1984,7 @@ class MyDecoratedService(remote.Service): pass api_info = MyDecoratedService.api_info - self.assertEqual('CoolService', api_info.name) + self.assertEqual('coolservice', api_info.name) self.assertEqual('vX', api_info.api_version) self.assertEqual('vX', api_info.path_version) self.assertEqual('My Cool Service', api_info.description) @@ -2002,13 +2003,13 @@ class MyDecoratedService(remote.Service): def testApiInfoDefaults(self): - @api_config.api('CoolService2', 'v2') + @api_config.api('coolservice2', 'v2') class MyDecoratedService(remote.Service): """Describes MyDecoratedService.""" pass api_info = MyDecoratedService.api_info - self.assertEqual('CoolService2', api_info.name) + self.assertEqual('coolservice2', api_info.name) self.assertEqual('v2', api_info.api_version) self.assertEqual('v2', api_info.path_version) self.assertEqual(None, api_info.description) @@ -2021,7 +2022,7 @@ class MyDecoratedService(remote.Service): def testApiInfoInvalidNamespaceNoDomain(self): with self.assertRaises(api_exceptions.InvalidNamespaceException): - @api_config.api('CoolService2', 'v2', + @api_config.api('coolservice2', 'v2', namespace=api_config.Namespace(None, 'name', 'path')) class MyDecoratedService(remote.Service): """Describes MyDecoratedService.""" @@ -2030,7 +2031,7 @@ class MyDecoratedService(remote.Service): def testApiInfoInvalidNamespaceNoName(self): with self.assertRaises(api_exceptions.InvalidNamespaceException): - @api_config.api('CoolService2', 'v2', + @api_config.api('coolservice2', 'v2', namespace=api_config.Namespace('domain', None, 'path')) class MyDecoratedService(remote.Service): """Describes MyDecoratedService.""" @@ -2038,7 +2039,7 @@ class MyDecoratedService(remote.Service): def testApiInfoNamespaceDefaultPath(self): - @api_config.api('CoolService2', 'v2', + @api_config.api('coolservice2', 'v2', namespace=api_config.Namespace('domain', 'name', None)) class MyDecoratedService(remote.Service): """Describes MyDecoratedService.""" @@ -2051,7 +2052,7 @@ class MyDecoratedService(remote.Service): def testGetApiClassesSingle(self): """Test that get_api_classes works when one class has been decorated.""" - my_api = api_config.api(name='My Service', version='v1') + my_api = api_config.api(name='myservice', version='v1') @my_api class MyDecoratedService(remote.Service): @@ -2061,7 +2062,7 @@ class MyDecoratedService(remote.Service): def testGetApiClassesSingleCollection(self): """Test that get_api_classes works with the collection() decorator.""" - my_api = api_config.api(name='My Service', version='v1') + my_api = api_config.api(name='myservice', version='v1') @my_api.api_class(resource_name='foo') class MyDecoratedService(remote.Service): @@ -2071,7 +2072,7 @@ class MyDecoratedService(remote.Service): def testGetApiClassesMultiple(self): """Test that get_api_classes works with multiple classes.""" - my_api = api_config.api(name='My Service', version='v1') + my_api = api_config.api(name='myservice', version='v1') @my_api.api_class(resource_name='foo') class MyDecoratedService1(remote.Service): @@ -2090,7 +2091,7 @@ class MyDecoratedService3(remote.Service): def testGetApiClassesMixedStyles(self): """Test that get_api_classes works when decorated differently.""" - my_api = api_config.api(name='My Service', version='v1') + my_api = api_config.api(name='myservice', version='v1') # @my_api is equivalent to @my_api.api_class(). This is allowed, though # mixing styles like this shouldn't be encouraged. @@ -2109,6 +2110,27 @@ class MyDecoratedService3(remote.Service): self.assertEqual([MyDecoratedService1, MyDecoratedService2, MyDecoratedService3], my_api.get_api_classes()) + def testApiNameRestrictions(self): + + @api_config.api(name='coolservice', version='vX') + class MyDecoratedService(remote.Service): + """Describes MyDecoratedService.""" + pass + + api_info = MyDecoratedService.api_info + assert 'coolservice' == api_info.name + + with pytest.raises(api_exceptions.InvalidApiNameException): + @api_config.api('CoolService2', 'v2') + class MyDecoratedService(remote.Service): + """Describes MyDecoratedService.""" + pass + + with pytest.raises(api_exceptions.InvalidApiNameException): + @api_config.api('c' + 'o'*40 + 'l', 'v2') + class MyDecoratedService(remote.Service): + """Describes MyDecoratedService.""" + pass class MethodDecoratorTest(unittest.TestCase): @@ -2180,7 +2202,7 @@ def _several_underscores__in_various___places__(self): def testMethodInfoPopulated(self): - @api_config.api(name='CoolService', version='vX', + @api_config.api(name='coolservice', version='vX', description='My Cool Service', hostname='myhost.com') class MyDecoratedService(remote.Service): """Describes MyDecoratedService.""" @@ -2211,7 +2233,7 @@ def my_method(self): def testMethodInfoDefaults(self): - @api_config.api('CoolService2', 'v2') + @api_config.api('coolservice2', 'v2') class MyDecoratedService(remote.Service): """Describes MyDecoratedService.""" @@ -2241,7 +2263,7 @@ class MyRequest(messages.Message): dog = messages.StringField(3) panda = messages.StringField(4, required=True) - @api_config.api('CoolService3', 'v3') + @api_config.api('coolservice3', 'v3') class MyDecoratedService(remote.Service): """Describes MyDecoratedService.""" @@ -2346,7 +2368,7 @@ def TryListAttributeVariations(self, attribute_name, config_name, api_kwargs = {attribute_name: api_value} method_kwargs = {attribute_name: method_value} - @api_config.api('AuthService', 'v1', hostname='example.appspot.com', + @api_config.api('authservice', 'v1', hostname='example.appspot.com', **api_kwargs) class AuthServiceImpl(remote.Service): """Describes AuthServiceImpl.""" @@ -2363,7 +2385,7 @@ def baz(self): generator = ApiConfigGenerator() api = json.loads(generator.pretty_print_config_to_json(AuthServiceImpl)) expected = { - 'authService.baz': { + 'authservice.baz': { 'httpMethod': 'POST', 'path': 'baz', 'request': {'body': 'empty'}, @@ -2375,9 +2397,9 @@ def baz(self): } } if expected_value: - expected['authService.baz'][config_name] = expected_value - elif config_name in expected['authService.baz']: - del expected['authService.baz'][config_name] + expected['authservice.baz'][config_name] = expected_value + elif config_name in expected['authservice.baz']: + del expected['authservice.baz'][config_name] test_util.AssertDictEqual(expected, api['methods'], self) diff --git a/endpoints/test/apiserving_test.py b/endpoints/test/apiserving_test.py index 668940a..3d32278 100644 --- a/endpoints/test/apiserving_test.py +++ b/endpoints/test/apiserving_test.py @@ -123,7 +123,7 @@ def delete(self, unused_request): return message_types.VoidMessage() -my_api = api_config.api(name='My Service', version='v1') +my_api = api_config.api(name='myservice', version='v1') @my_api.api_class() diff --git a/endpoints/test/openapi_generator_test.py b/endpoints/test/openapi_generator_test.py index f2ffa6a..58a09fb 100644 --- a/endpoints/test/openapi_generator_test.py +++ b/endpoints/test/openapi_generator_test.py @@ -154,6 +154,7 @@ def entries_post_audiences(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -480,6 +481,7 @@ def items_put_container(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1031,6 +1033,7 @@ def get_airport_2(self, request): 'title': 'iata', 'version': 'v1', }, + 'x-google-api-name': 'iata', 'host': None, 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1124,6 +1127,7 @@ def noop_get(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'localhost:8080', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1187,6 +1191,7 @@ def noop_get(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1287,6 +1292,7 @@ def override_get(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1359,6 +1365,7 @@ def noop_get(self, unused_request): 'description': 'Describes MyService.', 'version': '1.3.4', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1411,6 +1418,7 @@ def noop_get(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1463,6 +1471,7 @@ def noop_get(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1515,6 +1524,7 @@ def toplevel(self, unused_request): 'description': 'Testing repeated params', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1596,6 +1606,7 @@ def toplevel(self, unused_request): 'description': 'Testing repeated simple field params', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1671,6 +1682,7 @@ def toplevel(self, unused_request): 'description': 'Testing repeated Message params', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1768,6 +1780,7 @@ def noop_get(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -1833,6 +1846,7 @@ def entries_post_audience(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -2030,6 +2044,7 @@ def entries_post(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], @@ -2116,6 +2131,7 @@ def entries_post(self, unused_request): 'description': 'Describes MyService.', 'version': 'v1', }, + 'x-google-api-name': 'root', 'host': 'example.appspot.com', 'consumes': ['application/json'], 'produces': ['application/json'], diff --git a/endpoints/test/users_id_token_test.py b/endpoints/test/users_id_token_test.py index 2489cb2..861282a 100644 --- a/endpoints/test/users_id_token_test.py +++ b/endpoints/test/users_id_token_test.py @@ -550,7 +550,7 @@ class UsersIdTokenTestWithSimpleApi(UsersIdTokenTestBase): # pylint: disable=g-bad-name - @api_config.api('TestApi', 'v1') + @api_config.api('testapi', 'v1') class TestApiAnnotatedAtMethod(remote.Service): """Describes TestApi.""" @@ -563,7 +563,7 @@ def method(self): pass @api_config.api( - 'TestApi', 'v1', audiences=UsersIdTokenTestBase._SAMPLE_AUDIENCES, + 'testapi', 'v1', audiences=UsersIdTokenTestBase._SAMPLE_AUDIENCES, allowed_client_ids=UsersIdTokenTestBase._SAMPLE_ALLOWED_CLIENT_IDS) class TestApiAnnotatedAtApi(remote.Service): """Describes TestApi.""" @@ -639,7 +639,7 @@ def testMaybeSetVarsWithActualRequestAccessToken(self, mock_local, mock_get_clie dummy_email = 'test@gmail.com' dummy_client_id = self._SAMPLE_ALLOWED_CLIENT_IDS[0] - @api_config.api('TestApi', 'v1', + @api_config.api('testapi', 'v1', allowed_client_ids=self._SAMPLE_ALLOWED_CLIENT_IDS, scopes=[dummy_scope]) class TestApiScopes(remote.Service):