Commits

Anonymous committed 14e41e0

Added multi-service mapping function

Comments (0)

Files changed (3)

python/protorpc/webapp_test_util.py

 class WebServerTestBase(test_util.TestCase):
 
   USE_URLFETCH = False
+  SERVICE_PATH = '/my/service'
 
   def setUp(self):
 
     if not self.USE_URLFETCH:
       transport.urlfetch = self._original_urlfetch
 
-  def ResetServer(self):
+  def ResetServer(self, application=None):
     if self.server:
       self.server.shutdown()
 
     self.port = test_util.pick_unused_port()
-    self.server, self.application = self.StartWebServer(self.port)
+    self.server, self.application = self.StartWebServer(self.port, application)
 
     self.connection = self.CreateTransport(self.service_url)
 
     """Create a new transportation object."""
     return transport.HttpTransport(service_url, protocol=protojson)
 
-  def StartWebServer(self, port):
+  def StartWebServer(self, port, application=None):
     """Start web server."""
-    application = self.CreateWsgiApplication()
+    if not application:
+      application = self.CreateWsgiApplication()
     validated_application = validate.validator(application)
     server = simple_server.make_server('localhost', port, validated_application)
     server = ServerThread(server)
     server.wait_until_running()
     return server, application
 
+  def make_service_url(self, path):
+    return 'http://localhost:%d%s' % (self.port, path)
+
   @property
   def service_url(self):
-    return 'http://localhost:%d/my/service' % self.port
+    return self.make_service_url(self.SERVICE_PATH)
 
 
 class EndToEndTestBase(WebServerTestBase):

python/protorpc/wsgi/service.py

 __author__ = 'rafek@google.com (Rafe Kaplan)'
 
 import cgi
+import cStringIO
 import httplib
 import logging
 import re
 from .. import protobuf
 from .. import protojson
 from .. import messages
+from .. import registry
 from .. import remote
 from .. import util
 from . import util as wsgi_util
 
 __all__ = [
+  'DEFAULT_REGISTRY_PATH',
   'service_app',
 ]
 
 _HTTP_NOT_FOUND = wsgi_util.error(httplib.NOT_FOUND)
 _HTTP_UNSUPPORTED_MEDIA_TYPE = wsgi_util.error(httplib.UNSUPPORTED_MEDIA_TYPE)
 
+DEFAULT_REGISTRY_PATH = '/protorpc'
+
 
 @util.positional(2)
 def service_mapping(service_factory, service_path=r'.*', protocols=None):
 
   service_class = getattr(service_factory, 'service_class', service_factory)
   remote_methods = service_class.all_remote_methods()
-  path_matcher = re.compile(_REQUEST_PATH_PATTERN % service_path)  
+  path_matcher = re.compile(_REQUEST_PATH_PATTERN % service_path)
 
   def protorpc_service_app(environ, start_response):
     """Actual WSGI application function."""
 
   # Return WSGI application.
   return protorpc_service_app
+
+
+@util.positional(1)
+def service_mappings(services, registry_path=DEFAULT_REGISTRY_PATH):
+  """Create multiple service mappings with optional RegistryService.
+
+  Use this function to create single WSGI application that maps to
+  multiple ProtoRPC services plus an optional RegistryService.
+
+  Example:
+    services = service.service_mappings(
+        [(r'/time', TimeService),
+         (r'/weather', WeatherService)
+        ])
+
+    In this example, the services WSGI application will map to two services,
+    TimeService and WeatherService to the '/time' and '/weather' paths
+    respectively.  In addition, it will also add a ProtoRPC RegistryService
+    configured to serve information about both services at the (default) path
+    '/protorpc'.
+
+  Args:
+    services: If a dictionary is provided instead of a list of tuples, the
+      dictionary item pairs are used as the mappings instead.
+      Otherwise, a list of tuples (service_path, service_factory):
+      service_path: The path to mount service on.
+      service_factory: A service class or service instance factory.
+    registry_path: A string to change where the registry is mapped (the default
+      location is '/protorpc').  When None, no registry is created or mounted.
+
+  Returns:
+    WSGI application that serves ProtoRPC services on their respective URLs
+    plus optional RegistryService.
+  """
+  if isinstance(services, dict):
+    services = services.iteritems()
+
+  final_mapping = []
+  paths = set()
+  registry_map = {} if registry_path else None
+
+  for service_path, service_factory in services:
+    try:
+      service_class = service_factory.service_class
+    except AttributeError:
+      service_class = service_factory
+
+    if service_path not in paths:
+      paths.add(service_path)
+    else:
+      raise remote.ServiceConfigurationError(
+        'Path %r is already defined in service mapping' %
+        service_path.encode('utf-8'))
+
+    if registry_map is not None:
+      registry_map[service_path] = service_class
+
+    final_mapping.append(service_mapping(service_factory, service_path))
+
+  if registry_map is not None:
+    final_mapping.append(service_mapping(
+      registry.RegistryService.new_factory(registry_map), registry_path))
+
+  return wsgi_util.first_found(final_mapping)
+

python/protorpc/wsgi/service_test.py

 from protorpc import end2end_test
 from protorpc import protojson
 from protorpc import remote
+from protorpc import registry
 from protorpc import transport
 from protorpc import test_util
 from protorpc import webapp_test_util
 from protorpc.wsgi import service
+from protorpc.wsgi import util
 
 
-class ProtoRpcServiceTest(end2end_test.EndToEndTest):
+class ServiceMappingTest(end2end_test.EndToEndTest):
 
   def setUp(self):
     self.protocols = None
-    super(ProtoRpcServiceTest, self).setUp()
+    super(ServiceMappingTest, self).setUp()
+
+  def CreateServices(self):
+
+    return my_service, my_other_service
 
   def  CreateWsgiApplication(self):
     """Create WSGI application used on the server side for testing."""
       '/my/other_service',
       protocols=self.protocols)
 
-    def request_router(environ, start_response):
-      path_info = environ['PATH_INFO']
-      if path_info.startswith('/my/service'):
-        return my_service(environ, start_response)
-      elif path_info.startswith('/my/other_service'):
-        return my_other_service(environ, start_response)
-      raise AssertionError('Should never get here')
-    return request_router
+    return util.first_found([my_service, my_other_service])
 
   def testAlternateProtocols(self):
     self.protocols = remote.Protocols()
     self.stub.optional_message(string_value='alternate-protocol')
 
 
+class ProtoServiceMappingsTest(ServiceMappingTest):
+
+  def  CreateWsgiApplication(self):
+    """Create WSGI application used on the server side for testing."""
+    return service.service_mappings(
+      [('/my/service', webapp_test_util.TestService),
+       ('/my/other_service',
+        webapp_test_util.TestService.new_factory('initialized'))
+      ])
+
+  def GetRegistryStub(self, path='/protorpc'):
+    service_url = self.make_service_url(path)
+    transport = self.CreateTransport(service_url)
+    return registry.RegistryService.Stub(transport)
+
+  def testRegistry(self):
+    registry_client = self.GetRegistryStub()
+    services = registry_client.services()
+    self.assertEquals(
+      registry.ServicesResponse(
+        services=[
+          registry.ServiceMapping(
+            name='/my/other_service',
+            definition='protorpc.webapp_test_util.TestService'),
+          registry.ServiceMapping(
+            name='/my/service',
+            definition='protorpc.webapp_test_util.TestService'),
+          ]),
+      services)
+
+  def testRegistryDictionary(self):
+    self.ResetServer(service.service_mappings(
+      {'/my/service': webapp_test_util.TestService,
+       '/my/other_service':
+           webapp_test_util.TestService.new_factory('initialized'),
+      }))
+    registry_client = self.GetRegistryStub()
+    services = registry_client.services()
+    self.assertEquals(
+      registry.ServicesResponse(
+        services=[
+          registry.ServiceMapping(
+            name='/my/other_service',
+            definition='protorpc.webapp_test_util.TestService'),
+          registry.ServiceMapping(
+            name='/my/service',
+            definition='protorpc.webapp_test_util.TestService'),
+          ]),
+      services)
+
+  def testNoRegistry(self):
+    self.ResetServer(service.service_mappings(
+      [('/my/service', webapp_test_util.TestService),
+       ('/my/other_service',
+        webapp_test_util.TestService.new_factory('initialized'))
+      ],
+      registry_path=None))
+    registry_client = self.GetRegistryStub()
+    self.assertRaisesWithRegexpMatch(
+      remote.ServerError,
+      'HTTP Error 404: Not Found',
+      registry_client.services)
+
+  def testAltRegistry(self):
+    self.ResetServer(service.service_mappings(
+      [('/my/service', webapp_test_util.TestService),
+       ('/my/other_service',
+        webapp_test_util.TestService.new_factory('initialized'))
+      ],
+      registry_path='/registry'))
+    registry_client = self.GetRegistryStub('/registry')
+    services = registry_client.services()
+    self.assertEquals(
+      registry.ServicesResponse(
+        services=[
+          registry.ServiceMapping(
+            name='/my/other_service',
+            definition='protorpc.webapp_test_util.TestService'),
+          registry.ServiceMapping(
+            name='/my/service',
+            definition='protorpc.webapp_test_util.TestService'),
+          ]),
+      services)
+
+  def testDuplicateRegistryEntry(self):
+    self.assertRaisesWithRegexpMatch(
+      remote.ServiceConfigurationError,
+      "Path '/my/service' is already defined in service mapping",
+      service.service_mappings,
+      [('/my/service', webapp_test_util.TestService),
+       ('/my/service',
+        webapp_test_util.TestService.new_factory('initialized'))
+      ])
+
+  def testRegex(self):
+    self.ResetServer(service.service_mappings(
+      [('/my/[0-9]+', webapp_test_util.TestService.new_factory('service')),
+       ('/my/[a-z]+',
+            webapp_test_util.TestService.new_factory('other-service')),
+      ]))
+    my_service_url = 'http://localhost:%d/my/12345' % self.port
+    my_other_service_url = 'http://localhost:%d/my/blarblar' % self.port
+
+    my_service = webapp_test_util.TestService.Stub(
+      transport.HttpTransport(my_service_url))
+    my_other_service = webapp_test_util.TestService.Stub(
+      transport.HttpTransport(my_other_service_url))
+
+    response = my_service.init_parameter()
+    self.assertEquals('service', response.string_value)
+
+    response = my_other_service.init_parameter()
+    self.assertEquals('other-service', response.string_value)
+
+
 def main():
   unittest.main()