Commits

Anonymous committed dc427b1

FAIL: implementation progress evidence only
Adding basic configuration of TicketResource (nothing working on it yet)
Added a basic implementation of TicketSearchResource responding to urls GET /query and GET /query/criteria
Test cases added to make RestModule actually walk through the path of match_request() and process_request() with sample TicketSearchResource,
adjustments made for existent bugs and unfinished work in both operations

  • Participants
  • Parent commits d5c4aff

Comments (0)

Files changed (11)

File trac-dev/restontrac/tests/__init__.py

-#!/usr/bin/env python
-# -*- coding: UTF-8 -*-
-
-# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
-#
-#   Licensed under the Apache License, Version 2.0 (the "License");
-#   you may not use this file except in compliance with the License.
-#   You may obtain a copy of the License at
-#
-#       http://www.apache.org/licenses/LICENSE-2.0
-#
-#   Unless required by applicable law or agreed to in writing, software
-#   distributed under the License is distributed on an "AS IS" BASIS,
-#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-#   See the License for the specific language governing permissions and
-#   limitations under the License.
-
-
-r"""Publish Trac data via a REST-ful interface
-
-RESTful interface built on top of Trac RpcPlugin
-
-Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
-Licensed under the Apache License
-"""
-__author__ = 'Olemis Lang'
-
-# Ignore errors to avoid Internal Server Errors
-from trac.core import TracError
-TracError.__str__ = lambda self: unicode(self).encode('ascii', 'ignore')
-
-try:
-    from restontrac.tracrest import *
-    msg = 'Ok'
-except Exception, exc:
-#    raise
-    msg = "Exception %s raised: '%s'" % (exc.__class__.__name__, str(exc))

File trac-dev/restontrac/tests/test_resource_routes.py

-#!/usr/bin/env python
-# -*- coding: UTF-8 -*-
-
-#  Licensed to the Apache Software Foundation (ASF) under one
-#  or more contributor license agreements.  See the NOTICE file
-#  distributed with this work for additional information
-#  regarding copyright ownership.  The ASF licenses this file
-#  to you under the Apache License, Version 2.0 (the
-#  "License"); you may not use this file except in compliance
-#  with the License.  You may obtain a copy of the License at
-#
-#   http://www.apache.org/licenses/LICENSE-2.0
-#
-#  Unless required by applicable law or agreed to in writing,
-#  software distributed under the License is distributed on an
-#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-#  KIND, either express or implied.  See the License for the
-#  specific language governing permissions and limitations
-#  under the License.
-
-"""Tests for url mappings to IRestResource instances """
-
-import unittest, tempfile
-from trac.test import EnvironmentStub
-#TODO absolute imports
-from restontrac.tracrest.api import RestSystem
-from restontrac.tracrest.web_ui import RestModule
-from restontrac.tracrest.resources import RestResourceBase
-
-class NestedResource(RestResourceBase):
-    """ Sample implementation for validating the routing of
-        nested resources like ticket->attachments
-        Or in the future Product -> Tickets
-    """
-    def __init__(self):
-        self.setup("attachments","attachment",
-                    resource_name='project_attachments',
-                    parent_collection="/projects",
-                    parent_resource="project")
-
-class AutoconfigResource(RestResourceBase):
-    """ Sample implementation for validating the routing of
-        autoconfigurable resources as the case of Tickets
-        Wich will have a collection and a member resource
-        providing a standard API and implementation for this
-        combination of Collection/member resources
-    """
-    def __init__(self):
-        self.setup("tickets","ticket")
-
-class CustomMappingResource(RestResourceBase):
-    """ Sample implementation for validating the routing of
-        some custom special resources, that won't provide the
-        standard set of functionalities, but could rather
-        publish some single Http method instead of the complete
-        list
-        Verifies the mapping of a custom /prototype url since the
-        internal mapping here does not take place using
-        routes.Mapper.resource() but util.map_resource_path()
-    """
-    def __init__(self):
-        self.setup("milestones","milestone")
-
-    def supported_paths(self):
-        collection_url = "".join(["/",self.collection_name])
-        return {collection_url:('GET','POST'),
-                collection_url + "/{id}":('GET'),
-                collection_url + "/prototype":('GET')
-               }
-    def get_custom_actions_map(self):
-        return {(self.collection_name + "/prototype",'GET'): "prototype"}
-
-
-class ResourceRouteTests(unittest.TestCase):
-    """Unit tests covering the Product model"""
-    def setUp(self):
-        self.env = EnvironmentStub(enable=['trac.*', 'tracrest.*','api.*','resources','util',
-                                           'web_ui.*','NestedResource','AutoconfigResource',
-                                           'CustomMappingResource','RestModule','RestSystem'])
-        self.env.enable_component(NestedResource)
-        self.env.enable_component(AutoconfigResource)
-        self.env.enable_component(CustomMappingResource)
-        self.env.enable_component(RestModule)
-        self.env.enable_component(RestSystem)
-        self.rest_module = RestModule(self.env)
-        self.mapper = RestModule(self.env)._mapper
-        self.env.path = tempfile.mkdtemp('tracrest-tempenv')
-        #For the moment there is database dependency within these test cases
-        #self.env.reset_db()
-
-    def _verify_match(self, path, match_expected, method, action=None, id=None):
-        """Utility operation for the execution of the tests
-           and verification of results
-        """
-        match = self.mapper.match(path, {"REQUEST_METHOD": method})
-        route = self.mapper.routematch(path, {"REQUEST_METHOD": method})
-        call_type = ""
-        if route:
-            call_type = 'COLLECTION CALL' if ':(id)' in route[1].routepath else 'MEMBER CALL'
-        match_data = match or 'NOT SUPPORTED'
-        #TODO log this line
-        print "%s %s-> %s %s" % (call_type, method, path, str(match_data))
-        if match_expected:
-            self.assertTrue(match)
-            if action:
-                self.assertEquals(match.get('action'),action)
-            if id:
-                self.assertEqual(match.get('id'),id)
-        else:
-            self.assertFalse(match)
-
-        return match
-
-    def test_nested_resources(self):
-        """Test case for the functioning of the mapping of nested resources
-           Configurations tested in this test case were provided
-           by NestedResource test component within the present module
-        """
-        #TODO document expected results within comments
-        route_vars = self._verify_match("/projects/26/attachments/51",True,'GET')
-        self.assertEquals(route_vars.get('controller'),'project_attachments')
-        self.assertEquals(route_vars.get('project_id'),'26')
-        self.assertEquals(route_vars.get('id'),'51')
-
-        self._verify_match("/projects/",False,'GET')
-
-        self._verify_match("/projects/26/attachments",True,'POST')
-        self._verify_match("/projects/26/attachments",True,'GET')
-        self._verify_match("/projects/26/attachments/45",True,'PUT')
-        self._verify_match("/projects/26/attachments",False,'PUT')
-
-    def test_resource_autoconfig(self):
-        """Test case to verify the expected functioning of resources url
-           auto-configuration.
-           Configurations tested in this test case were provided
-           by AutoconfigResource test component within the present module
-        """
-        #verifying the reading of the collection, by default operation will be called
-        # 'list' for auto-configured resources
-        self._verify_match("/tickets", True, "GET",action='list')
-        # Since no "new" resource url is supported, in a call to "resource/new"
-        # 'new' must be interpreted as the member id matching resource/{id}
-        match = self._verify_match("/tickets/new", True, "GET",action='read', id='new')
-        self.assertTrue(match.get("id") == 'new')
-        #No post to the member allowed, only GET, PUT, DELETE
-        self._verify_match("/tickets/new", False, "POST")
-        #verifying the route mapping for create operation in the collection
-        self._verify_match("/tickets", True, "POST",action='create')
-        #No post allowed to the member resource
-        self._verify_match("/tickets/25", False, "POST")
-        #route mapping for to read the member
-        match = self._verify_match("/tickets/25", True, "GET", action='read',id='25')
-        #route mapping for update operations on the member
-        self._verify_match("/tickets/25", True, "PUT", action='update',id='25')
-        #verifying delete method on the member
-        self._verify_match("/tickets/25", True, "DELETE", action='delete',id='25')
-        #No update allowed for the collection
-        self._verify_match("/tickets", False, "PUT")
-        #No delete allowed for the collection
-        self._verify_match("/tickets", False, "DELETE")
-
-        # for the moment will allow to specify the format as extensions
-        # although request header content-type will be supported as well
-        match = self._verify_match("/tickets.xml", True, "GET")
-        self.assertTrue(match.get('format') == 'xml')
-
-        #checking the format does not interfere with id extraction for read paths
-        match = self._verify_match("/tickets/25.xml", True, "GET",id='25')
-
-        #checking the format does not interfere with id extraction for update paths
-        match = self._verify_match("/tickets/25.xml", True, "PUT", id='25')
-
-        # checking that auto-configured resources will have the
-        # /prototype resource within the collection resource to obtain
-        # a copyable instance of the member resource initialized with
-        # default values to be used in create operations.
-        # /prototype is a symbolic resource that one should GET, to have
-        # default values already filled in, to change only desired values
-        # and send it back to the server within a POST to the collection
-        match = self._verify_match("/tickets/prototype", True, "GET", action='prototype')
-        #/prototype shouldn't be interpreted as an id but as a particular resource
-        self.assertFalse(match.get('id',None))
-
-    def test_resource_supported_paths(self):
-        """Special test case to verify the functioning of
-           custom paths configuration that do not take place automatically
-           by RestResourceBase.map_resources().
-           When the resource does not accomplish the
-           standard interface of a collection/member resource
-           then it must provide a list of supported paths allong with
-           the supported http methods for each one
-           Additional the resource might implement get_custom_actions_map()
-           to override the default mapping from http methods to actions so
-           it's possible to indicate that a call to GET in some url should
-           be forwarded to an operation 'do_something()' instead of 'read()'
-           Configurations tested in this test case were provided
-           by CustomMappingResource test component within the present module
-        """
-        self._verify_match("/milestones", True, "GET")
-        #a call to resource/new must be interpreted as /resource{id}
-        #with 'new' as the id value
-        self._verify_match("/milestones/new", True, "GET",id='new')
-        #No post to the member configured
-        self._verify_match("/milestones/new", False, "POST")
-        #POST was configured for the collection, must be allowed
-        self._verify_match("/milestones", True, "POST")
-        #No post to the member configured
-        self._verify_match("/milestones/25", False, "POST")
-        #GET was configured, must be allowed
-        self._verify_match("/milestones/25", True, "GET")
-        #No put allowed for the configuration provided
-        self._verify_match("/milestones/25", False, "PUT")
-        #No delete allowed for the configuration provided
-        self._verify_match("/milestones/25", False, "DELETE")
-        #No put allowed for the configuration provided
-        self._verify_match("/milestones", False, "PUT")
-        #No delete allowed for the configuration provided
-        self._verify_match("/milestones", False, "DELETE")
-        #No configuration of format provided
-        self._verify_match("/milestones.xml", False, "GET")
-        #As configured this resource allows no format so any extension will
-        # be interpreted as part if the id
-        self._verify_match("/milestones/25.xml", True, "GET", id='25.xml')
-        #PUT wasn't included in supported_paths()
-        self._verify_match("/milestones/25.xml", False, "PUT")
-
-        # verifying 'prototype' is not interpreted as an id,
-        # since it was provided as a supported path instead
-        match = self._verify_match("/milestones/prototype", True, "GET")
-        self.assertFalse(match.get('id',None))
-
-        #The custom action 'prototype' was configured through
-        # get_custom_actions_map() of CustomMappingResource
-        self.assertTrue(match.get('action','prototype'))
-
-        # only GET configured in this test case,
-        # '/prototype' is not a member but a special resource
-        # to copy it before creating a new member
-        self._verify_match("/milestones/prototype", False, "PUT")
-
-    def tearDown(self):
-        pass
-

File trac-dev/restontrac/tracrest/__init__.py

 TracError.__str__ = lambda self: unicode(self).encode('ascii', 'ignore')
 
 try:
-    from restontrac.tracrest import *
+    from tracrest import *
     msg = 'Ok'
 except Exception, exc:
 #    raise

File trac-dev/restontrac/tracrest/api.py

            In correspondance with the content-type specified
            the corresponding Formatter instance is used calling
            parse(), to decode the received data
+           returns a dict with parsed request data
         """
-    def format(self, request):
-        """Used when a response is received
-           In correspondance with the content-type specified
-           the corresponding Formatter instance is used calling
-           parse(), to encode the returned data
+    def format(self, result):
+        """Used when a response is about to be sent
+           returns the result formatted on the supported
+		   content type
         """
     def supported_ct(self):
         """Returns the supported content-type to wich the current
     def __init__(self):
         super(RestSystem, self).__init__()
 
-    def resource_names(self):
-        return [r.get_name() for r in self.resources]
+    def resources_dict(self):
+        return dict([(r.get_name(),r) for r in self.resources])
+
+    def get_resource(self, res_name):
+        return self.resources_dict().get(res_name,None)
 
     def resource_path_dict(self):
         """Returns a map of every configured path with
         #TODO unfinished work, do not review this
         pass
 
-    def call_resource(self, req, resource, method, args, kwargs):
+    def call_resource(self, req, resource, *args, **kwargs):
         #TODO unfinished work, do not review this
-        if method in resource.supported_operations():
-            callable = resource.__getattr__(method)
-            result = callable(req, args, kwargs)
-            #if isinstance(result, GeneratorType):
-            #    result = list(result)
+            operation = req.args['action']
+            if operation:
+                callable = getattr(resource, operation)
+                result = callable(req, **kwargs)
+                #if isinstance(result, GeneratorType):
+                #    result = list(result)
+                return result
+            else:
+                #TODO raise routing error, not operation specified
+                pass
 

File trac-dev/restontrac/tracrest/resources.py

 from trac.core import implements
 from trac.core import Component
 #TODO absolute imports
-from api import IRestResource, IRoutesResourceMapper
+from api import IRestResource, IRoutesResourceMapper, \
+    IFormatter
 
 #default http methods supported by collections
 def_coll_methods = ['GET','POST', 'OPTIONS']
            by default no formatter is specified
         """
         return None
+
+    def extract_extra_params(self, req):
+        """ Default implementation to prevent errors
+		    when extending components do not implement
+			any logic for extra params extraction
+		"""
+		return {}
+
+
+
+class PlanTextFormat(Component):
+    """ Implementation of the default formatter for 
+	    text/html content type
+	"""
+	#TODO verify if it's really needed and if it must be placed somewhere else
+	implements(IFormatter)
+
+    def parse(self, request):
+        #TODO check what must be done
+        return {}
+
+    def format(self, result):
+        #TODO check what must be done
+        return result
+
+    def supported_ct(self):
+        return 'text/html'
+

File trac-dev/restontrac/tracrest/tests/__init__.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+r"""Publish Trac data via a REST-ful interface
+
+RESTful interface built on top of Trac RpcPlugin
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License
+"""
+__author__ = 'Olemis Lang'
+
+# Ignore errors to avoid Internal Server Errors
+from trac.core import TracError
+TracError.__str__ = lambda self: unicode(self).encode('ascii', 'ignore')
+
+try:
+    from restontrac.tracrest import *
+    msg = 'Ok'
+except Exception, exc:
+#    raise
+    msg = "Exception %s raised: '%s'" % (exc.__class__.__name__, str(exc))

File trac-dev/restontrac/tracrest/tests/test_resource_routes.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+r"""Publish Trac data via a REST-ful interface
+
+RESTful interface built on top of Trac RpcPlugin
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License
+"""
+import unittest, tempfile
+from trac.test import EnvironmentStub
+#TODO absolute imports
+from restontrac.tracrest.api import RestSystem
+from restontrac.tracrest.web_ui import RestModule
+from restontrac.tracrest.resources import RestResourceBase
+
+class NestedResource(RestResourceBase):
+    """ Sample implementation for validating the routing of
+        nested resources like ticket->attachments
+        Or in the future Product -> Tickets
+    """
+    def __init__(self):
+        self.setup("attachments","attachment",
+                    resource_name='project_attachments',
+                    parent_collection="/projects",
+                    parent_resource="project")
+
+class AutoconfigResource(RestResourceBase):
+    """ Sample implementation for validating the routing of
+        autoconfigurable resources as the case of Tickets
+        Wich will have a collection and a member resource
+        providing a standard API and implementation for this
+        combination of Collection/member resources
+    """
+    def __init__(self):
+        self.setup("tickets","ticket")
+
+class CustomMappingResource(RestResourceBase):
+    """ Sample implementation for validating the routing of
+        some custom special resources, that won't provide the
+        standard set of functionalities, but could rather
+        publish some single Http method instead of the complete
+        list
+        Verifies the mapping of a custom /prototype url since the
+        internal mapping here does not take place using
+        routes.Mapper.resource() but util.map_resource_path()
+    """
+    def __init__(self):
+        self.setup("milestones","milestone")
+
+    def supported_paths(self):
+        collection_url = "".join(["/",self.collection_name])
+        return {collection_url:('GET','POST'),
+                collection_url + "/{id}":('GET'),
+                collection_url + "/prototype":('GET')
+               }
+    def get_custom_actions_map(self):
+        return {(self.collection_name + "/prototype",'GET'): "prototype"}
+
+
+class ResourceRouteTests(unittest.TestCase):
+    """Unit tests covering the Product model"""
+    def setUp(self):
+        self.env = EnvironmentStub(enable=['trac.*', 'tracrest.*','api.*','resources','util',
+                                           'web_ui.*','NestedResource','AutoconfigResource',
+                                           'CustomMappingResource','RestModule','RestSystem'])
+        self.env.enable_component(NestedResource)
+        self.env.enable_component(AutoconfigResource)
+        self.env.enable_component(CustomMappingResource)
+        self.env.enable_component(RestModule)
+        self.env.enable_component(RestSystem)
+        self.rest_module = RestModule(self.env)
+        self.mapper = RestModule(self.env)._mapper
+        self.env.path = tempfile.mkdtemp('tracrest-tempenv')
+        #For the moment there is no database dependency within these test cases
+        #self.env.reset_db()
+
+    def _verify_match(self, path, match_expected, method, action=None, id=None):
+        """Utility operation for the execution of the tests
+           and verification of results
+        """
+        match = self.mapper.match(path, {"REQUEST_METHOD": method})
+        route = self.mapper.routematch(path, {"REQUEST_METHOD": method})
+        call_type = ""
+        if route:
+            call_type = 'COLLECTION CALL' if ':(id)' in route[1].routepath else 'MEMBER CALL'
+        match_data = match or 'NOT SUPPORTED'
+        #TODO log this line
+        print "%s %s-> %s %s" % (call_type, method, path, str(match_data))
+        if match_expected:
+            self.assertTrue(match)
+            if action:
+                self.assertEquals(match.get('action'),action)
+            if id:
+                self.assertEqual(match.get('id'),id)
+        else:
+            self.assertFalse(match)
+
+        return match
+
+    def test_nested_resources(self):
+        """Test case for the functioning of the mapping of nested resources
+           Configurations tested in this test case were provided
+           by NestedResource test component within the present module
+        """
+        #TODO document expected results within comments
+        route_vars = self._verify_match("/projects/26/attachments/51",True,'GET')
+        self.assertEquals(route_vars.get('controller'),'project_attachments')
+        self.assertEquals(route_vars.get('project_id'),'26')
+        self.assertEquals(route_vars.get('id'),'51')
+
+        self._verify_match("/projects/",False,'GET')
+
+        self._verify_match("/projects/26/attachments",True,'POST')
+        self._verify_match("/projects/26/attachments",True,'GET')
+        self._verify_match("/projects/26/attachments/45",True,'PUT')
+        self._verify_match("/projects/26/attachments",False,'PUT')
+
+    def test_resource_autoconfig(self):
+        """Test case to verify the expected functioning of resources url
+           auto-configuration.
+           Configurations tested in this test case were provided
+           by AutoconfigResource test component within the present module
+        """
+        #verifying the reading of the collection, by default operation will be called
+        # 'list' for auto-configured resources
+        self._verify_match("/tickets", True, "GET",action='list')
+        # Since no "new" resource url is supported, in a call to "resource/new"
+        # 'new' must be interpreted as the member id matching resource/{id}
+        match = self._verify_match("/tickets/new", True, "GET",action='read', id='new')
+        self.assertTrue(match.get("id") == 'new')
+        #No post to the member allowed, only GET, PUT, DELETE
+        self._verify_match("/tickets/new", False, "POST")
+        #verifying the route mapping for create operation in the collection
+        self._verify_match("/tickets", True, "POST",action='create')
+        #No post allowed to the member resource
+        self._verify_match("/tickets/25", False, "POST")
+        #route mapping for to read the member
+        match = self._verify_match("/tickets/25", True, "GET", action='read',id='25')
+        #route mapping for update operations on the member
+        self._verify_match("/tickets/25", True, "PUT", action='update',id='25')
+        #verifying delete method on the member
+        self._verify_match("/tickets/25", True, "DELETE", action='delete',id='25')
+        #No update allowed for the collection
+        self._verify_match("/tickets", False, "PUT")
+        #No delete allowed for the collection
+        self._verify_match("/tickets", False, "DELETE")
+
+        # for the moment will allow to specify the format as extensions
+        # although request header content-type will be supported as well
+        match = self._verify_match("/tickets.xml", True, "GET")
+        self.assertTrue(match.get('format') == 'xml')
+
+        #checking the format does not interfere with id extraction for read paths
+        match = self._verify_match("/tickets/25.xml", True, "GET",id='25')
+
+        #checking the format does not interfere with id extraction for update paths
+        match = self._verify_match("/tickets/25.xml", True, "PUT", id='25')
+
+        # checking that auto-configured resources will have the
+        # /prototype resource within the collection resource to obtain
+        # a copyable instance of the member resource initialized with
+        # default values to be used in create operations.
+        # /prototype is a symbolic resource that one should GET, to have
+        # default values already filled in, to change only desired values
+        # and send it back to the server within a POST to the collection
+        match = self._verify_match("/tickets/prototype", True, "GET", action='prototype')
+        #/prototype shouldn't be interpreted as an id but as a particular resource
+        self.assertFalse(match.get('id',None))
+
+    def test_resource_supported_paths(self):
+        """Special test case to verify the functioning of
+           custom paths configuration that do not take place automatically
+           by RestResourceBase.map_resources().
+           When the resource does not accomplish the
+           standard interface of a collection/member resource
+           then it must provide a list of supported paths allong with
+           the supported http methods for each one
+           Additional the resource might implement get_custom_actions_map()
+           to override the default mapping from http methods to actions so
+           it's possible to indicate that a call to GET in some url should
+           be forwarded to an operation 'do_something()' instead of 'read()'
+           Configurations tested in this test case were provided
+           by CustomMappingResource test component within the present module
+        """
+        self._verify_match("/milestones", True, "GET")
+        #a call to resource/new must be interpreted as /resource{id}
+        #with 'new' as the id value
+        self._verify_match("/milestones/new", True, "GET",id='new')
+        #No post to the member configured
+        self._verify_match("/milestones/new", False, "POST")
+        #POST was configured for the collection, must be allowed
+        self._verify_match("/milestones", True, "POST")
+        #No post to the member configured
+        self._verify_match("/milestones/25", False, "POST")
+        #GET was configured, must be allowed
+        self._verify_match("/milestones/25", True, "GET")
+        #No put allowed for the configuration provided
+        self._verify_match("/milestones/25", False, "PUT")
+        #No delete allowed for the configuration provided
+        self._verify_match("/milestones/25", False, "DELETE")
+        #No put allowed for the configuration provided
+        self._verify_match("/milestones", False, "PUT")
+        #No delete allowed for the configuration provided
+        self._verify_match("/milestones", False, "DELETE")
+        #No configuration of format provided
+        self._verify_match("/milestones.xml", False, "GET")
+        #As configured this resource allows no format so any extension will
+        # be interpreted as part if the id
+        self._verify_match("/milestones/25.xml", True, "GET", id='25.xml')
+        #PUT wasn't included in supported_paths()
+        self._verify_match("/milestones/25.xml", False, "PUT")
+
+        # verifying 'prototype' is not interpreted as an id,
+        # since it was provided as a supported path instead
+        match = self._verify_match("/milestones/prototype", True, "GET")
+        self.assertFalse(match.get('id',None))
+
+        #The custom action 'prototype' was configured through
+        # get_custom_actions_map() of CustomMappingResource
+        self.assertTrue(match.get('action','prototype'))
+
+        # only GET configured in this test case,
+        # '/prototype' is not a member but a special resource
+        # to copy it before creating a new member
+        self._verify_match("/milestones/prototype", False, "PUT")
+
+    def tearDown(self):
+        pass
+

File trac-dev/restontrac/tracrest/tests/web_ui.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+r"""Publish Trac data via a REST-ful interface
+
+RESTful interface built on top of Trac RpcPlugin
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License
+"""
+
+from datetime import datetime, timedelta
+from pprint import pprint
+
+import unittest
+import tempfile
+import shutil
+from trac.test import EnvironmentStub, Mock, MockPerm
+from trac.ticket.api import TicketSystem
+from trac.ticket import Ticket
+from trac.util.datefmt import FixedOffset
+from trac.util import format_datetime
+from trac.web import Href
+#FIXME absolute imports
+from tracrest.web_ui import RestModule
+from tracrest.ticket_search import TicketSearchResource
+from tracrest.resources import PlanTextFormat
+REST_API_URL = "/api"
+BHSEARCH_URL = ''
+DEFAULT_DOCS_PER_PAGE = 10
+
+def mock_get(key, default=''):
+    if key == 'trac.base_url':
+        return "/localhost:8000/main"
+    else:
+        return ''
+
+
+class WebUiTestCase(unittest.TestCase):
+
+
+    def setUp(self):
+        self.env = EnvironmentStub(enable=['tracrest.*','tracrest.ticket_search.*',
+                                           'tracrest.ticket_search.TicketSearchResource',
+                                           'tracrest.resources.*',
+                                           'tracrpc.*'])
+        self.env.get = mock_get
+        self.env.path = tempfile.mkdtemp('tracrest-tempenv')
+
+        #FIXME don't know why EnvironmentStub's init method with 'enable' keyword argument seems not to enable this components
+        self.env.enable_component(TicketSearchResource)
+        self.env.enable_component(PlanTextFormat)
+
+        self.web_module = RestModule(self.env)
+        from trac.web.api import Request
+        environ = {'PATH_INFO':REST_API_URL,
+                   'SERVER_PORT':'8000',
+                   'wsgi.url_scheme':'http',
+                   'SERVER_NAME':'localhost',
+                   'REQUEST_METHOD':'GET'}
+        self.req = Mock(Request, environ,
+                        lambda *args, **kwds:
+                        lambda data: data,
+            perm=MockPerm(),
+            chrome={'logo': {}},
+            href=Href(REST_API_URL),
+            args={},
+        )
+#                self.req = Mock(href=self.env.href, authname='anonymous', tz=utc)
+#        self.req = Mock(base_path='/trac.cgi', path_info='',
+#                        href=Href('/trac.cgi'), chrome={'logo': {}},
+#                        abs_href=Href('http://example.org/trac.cgi'),
+#                        environ={}, perm=[], authname='-', args={}, tz=None,
+#                        locale='', session=None, form_token=None)
+
+#        self.req = Mock(href=self.env.href, abs_href=self.env.abs_href, tz=utc,
+#                        perm=MockPerm())
+#
+
+    def tearDown(self):
+        shutil.rmtree(self.env.path)
+        self.env.reset_db()
+
+    def _process_request(self):
+        if self.web_module.match_request(self.req):
+            response = self.web_module.process_request(self.req)
+            url, data, x = response
+            print "Received url: %s data:" % url
+            pprint(data)
+            if data.has_key("results"):
+                print "results :"
+                pprint(data["results"].__dict__)
+            return data
+        else:
+            return None
+
+    def _set_req_url(self, url, method='GET', args={}):
+        path = REST_API_URL+'/'+url
+        self.req.href = Href(path)
+        self.req.environ['PATH_INFO'] = path
+        if method:
+            self.req.environ['REQUEST_METHOD'] = method
+        self.req.args.update(args)
+
+    def test_search_criteria(self):
+        self._set_req_url('query/criteria')
+        data = self._process_request()
+        #TODO find a proper way to check results
+
+    def test_search(self):
+        #TODO use a valid queryName
+        self._set_req_url('query',args={'q':'queryName'})
+        data = self._process_request()
+        #TODO find a proper way to check results
+
+def suite():
+    return unittest.makeSuite(WebUiTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()

File trac-dev/restontrac/tracrest/ticket_search.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+r"""Publish Trac data via a REST-ful interface
+
+RESTful interface built on top of Trac RpcPlugin
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License
+"""
+
+from resources import RestResourceBase
+try:
+    from trac_rpcplugin.tracrpc.search import SearchRPC
+    rpc_exists = True
+except ImportError:
+    rpc_exists = None
+
+class TicketSearchResource(RestResourceBase):
+
+    abstract = False
+
+    def __init__(self):
+        #/ticket_search and /ticket_search/criteria
+        self.search_url = s_url = "".join(["/",'query'])
+        self.criteria_url = "/".join([s_url,'criteria'])
+        self.setup("query","criteria")
+
+    def supported_paths(self):
+        return {self.search_url:('GET',),
+                self.criteria_url:('GET',)}
+
+    def get_custom_actions_map(self):
+        return {(self.search_url,'GET'): "do_search",
+                (self.criteria_url,'GET'):"get_criteria"}
+
+    def available_operations(self, request):
+        #No matter the resource name requested, the only available operation is http GET
+        return ('GET',)
+
+    def do_search(self, req, **kwargs):
+        if not rpc_exists:
+            #TODO return a warning message indicating the RPC Plugin is not correctly installed
+            pass
+        try:
+            query = kwargs.get('query',None)
+            if not query:
+                #raise exception since query is required
+                pass
+            filters = kwargs.get('filters', None)
+            rpc_search = SearchRPC(self.env)
+            result = rpc_search.performSearch(req, query, filters)
+            #TODO restify results before returning i.e.
+            return result
+        except:
+            #TODO return a warning message indicating error calling the the RPC Plugin
+            pass
+
+    def get_criteria(self, req, **kwargs):
+        if not rpc_exists:
+            #TODO return a warning message indicating the RPC Plugin is not correctly installed
+            return "WARNING, no RPC PLugin installed"
+        try:
+            rpc_search = SearchRPC(self.env)
+            criteria = rpc_search.get_search_criteria(req)
+            return criteria
+        except:
+            #TODO return a warning message indicating error calling the the RPC Plugin
+            pass
+

File trac-dev/restontrac/tracrest/tickets.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+
+r"""Publish Trac data via a REST-ful interface
+
+RESTful interface built on top of Trac RpcPlugin
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License
+"""
+
+from resources import RestResourceBase
+
+class TicketResource(RestResourceBase):
+
+    def __init__(self):
+        self.setup("tickets","ticket")
+
+    def list(self):
+        """To list all tickets when executed a
+           GET request to /tickets url
+        """
+        return []
+
+    def read(self, request, *args, **kwargs):
+        """Method to handle Http GET request
+           to /tickets/{id}
+        """
+        return handler.get()
+
+    def create(self, request, *args, **kwargs):
+        """Method to handle Http POST requests
+           This will in general by translated into
+           a CREATE routine in CRUD terms
+           Specializations must implement specific logic
+           if not, unsupported opperation http error
+           will be returned to the client
+        """
+        NotImplementedError()
+
+    def update(self, request, *args, **kwargs):
+        """Method to handle Http POST requests
+           This will in general by translated into
+           an UPDATE routine in CRUD terms
+           Specializations must implement specific logic
+           if not, unsupported opperation http error
+           will be returned to the client
+        """
+        NotImplementedError()
+
+    def update_diff(self, request, *args, **kwargs):
+        """Method to handle Http PATCH requests
+           This will in general by translated into
+           an UPDATE routine in CRUD terms
+        """
+        #TODO evaluate if this operation must be supported
+
+    def delete(self, request, *args, **kwargs):
+        """Method to handle Http DELETE requests
+           This will in general by translated into
+           a DELETE routine in CRUD terms
+           Specializations must implement specific logic
+           if not, unsupported opperation http error
+           will be returned to the client
+        """
+        NotImplementedError()

File trac-dev/restontrac/tracrest/web_ui.py

     formatters = ExtensionPoint(IFormatter)
 
     def __init__(self):
-        self._mapper = RestMapper(controller_scan=RestSystem(self.env).resource_names())
+        self._mapper = RestMapper(controller_scan=RestSystem(self.env).resources_dict().iterkeys())
         self.setup()
+        self._formatters = None
 
     def setup(self):
         #mapping resorces to their corresponding paths
         # IRequestHandler methods
 
     def formatters_dict(self):
-        if not self._formatters and len(self.formatters) > 0:
-            self._formatters =  dict([(fmt.supported_ct, fmt)
+        if not self._formatters or len(self.formatters) > 0:
+            self._formatters =  dict([(fmt.supported_ct(), fmt)
                                       for fmt in self.formatters])
         return self._formatters
 
+    def get_formatter(self, content_type):
+        return self.formatters_dict().get(content_type, None)
+
     def _extract_request_method(self, req):
         """
            Not all clients support all http methods, that's why
         return method
 
     def match_request(self, req):
-        """ Look for available protocols serving at requested path and
-            content-type. """
-        match = re.match(r'^/api/([^/]+)$', req.path_info)
+        match = re.match(r'^/api/(.+)$', req.path_info)
         if not match:
             #TODO check if "/login/api" accomplishes REST guidelines
-            match = re.match(r'^/login/api/([^/]+)$', req.path_info)
+            match = re.match(r'^/login/api/(.+)$', req.path_info)
         if match:
             #TODO evaluate passing this logic as a first step in process_request()
             #will attempt to find the requested resource
             method = self._extract_request_method(req)
-            r_data = self._mapper.routematch(req.path_info,
-                environ={'method':method})
+            resource_path = "".join(['/' , match.group(1)])
+            r_data = self._mapper.routematch(resource_path,
+                environ={'REQUEST_METHOD':method})
             if r_data:
                 r_params = r_data[0]
                 #publishing the supported methods on the request
                 req.args['REST_REQUEST'] = \
                         'COLLECTION' if ':(id)' in r_data[1].routepath else 'MEMBER'
                 res_name = r_params.get('controller')
-                resource = self.resources_dict.get(res_name)
+                resource = RestSystem(self.env).get_resource(res_name)
                 #publishing in the request the params relative to the resource
-                req.args.expand(r_params)
+                req.args.update(r_params)
                 #publishing the corresponding resource
                 req.args['resource'] = resource
+                #publishing in the request the configured routes operation
+                req.args['action'] = r_params.get('action', None)
                 #Giving the resource a chance to make any request params pre-processing
                 extra_args = resource.extract_extra_params(req) or {}
-                req.args.expand(extra_args)
+                req.args.update(extra_args)
                 #Setting up the route's request config to enable the use of url_for()
                 setup_routes_config(self._mapper, req, r_params)
                 return True
         res = req.args.get('resource', None)
         content_type = req.get_header('Content-Type') or 'text/html'
         if res:
-            if req.method not in res.available_operations():
+            if req.method not in res.available_operations(req):
                 self._send_unsupported_error(req)
 
             # Perform the method call
 
             formatter = res.get_formatter(content_type)
             if not formatter:
-                formatter = self.formatters_dict(content_type, None)
+                formatter = self.get_formatter(content_type)
             if not formatter:
                 # Attempted to make a REST call for an unsupported content type
                 body = "Requested Content-Type  unsupported ('%s') at path '%s'." % \
                 req.send_error(None, template='', content_type='text/plain',
                     status=HTTPUnsupportedMediaType.code, env=None, data=body)
 
-            self._process_rest_call(req, res, formatter, req.method)
-
+            return self._process_rest_call(req, res, formatter)
         #TODO verify handling of headers ContentType and Accept
 
 
     def _process_rest_call(self, req, resource, formatter):
         """Process incoming REST request and finalize response."""
-        method = req.method.lower
+        method = req.method
 
         try :
-            self.log.debug("REST(%s) call by '%s'", repr(resource), req.authname)
+            self.log.debug("REST(%s) call by '%s'", repr(resource.get_name()), req.authname)
             #attemp to use the formatter specified by the resource if there is one
 
             rest_req = req.rest = formatter.parse(req)
             self.log.debug("REST(%s) call by '%s' %s", repr(resource), \
                                               req.authname, method)
             try :
-                result = RestSystem(self.env).call_resource(req, resource, method, args, kwargs)
-                result = formatter.format(result)
-                self._send_response(req, result, content_type)
+                if not method in resource.available_operations(req):
+                    pass
+                    #TODO send unsupported error
+                else:
+                    result = RestSystem(self.env).call_resource(req, resource, args, kwargs)
+                    result = formatter.format(result)
+                    self._send_response(req, result, content_type)
 
             except ( PermissionError, ResourceNotFound), e:
                 raise
                 self._send_unknown_error()
             except Exception:
                 e, tb = sys.exc_info()[-2:]
-                raise #ServiceException(e), None, tb
+                #TODO maybe encapsulate in an HTTP error
+                raise
         except RequestDone :
             #it means the resource raised the exception indicating it has processed the request completely
             raise
                 self.log.exception("REST(%s) Unhandled protocol error", resource.get_name())
                 self._send_unknown_error(req, e)
         except Exception, e :
-            self.log.exception("REST(%s) Unhandled protocol error", resource.get_name())
+            self.log.exception("REST(%s) Unhandled protocol error: %s", (resource.get_name(),e))
             self._send_unknown_error(req, e)
 
     def _send_response(self, req, response, content_type='application/json'):
-        self.log.debug("RPC(json) encoded response: %s" % response)
+        self.log.debug("REST(%s) encoded response: %s" % (content_type, response))
         response = to_unicode(response).encode("utf-8")
         req.send_response(200)
         req.send_header('Content-Type', content_type)
 
     def _send_unknown_error(self, req, e):
         """Last recourse if protocol cannot handle the RPC request | error"""
-        method_name = req.rpc and req.rpc.get('method') or '(undefined)'
-        body = "Unhandled protocol error calling '%s': %s" % (
-                                        method_name, to_unicode(e))
+        method = req.method
+        body = "Unhandled error calling %s.'%s': %s" % (
+                                        req.args['resource'],method, to_unicode(e))
         req.send_error(None, template='', content_type='text/plain',
                             env=None, data=body, status=HTTPInternalError.code)