"""A RESTful collection implementation for CherryPy 2.2.

Based on 'wsgicollection' (http://bitworking.org/news/wsgicollection).
"""

import cherrypy

class Collection(object):
    """A class representing a collection of items.
    
    Items can be created, updated, viewed, listed and deleted.  Dispatches
    based on HTTP method and URL fragments.
    
    Classes that need to implement collection-like behavior should inherit from
    this class and supply Python method for handling desired HTTP methods.
    
    Something to be aware of is that handlers for the various collection
    operations are not "exposed" in the traditional CherryPy sense.  Instead,
    they are mapped in the following manner:
    
    HTTP methods meant to operate on the collection and individual collection
    items are mapped to handlers in the col_dict and item_dict class attributes,
    respectively.

    When a GET request is made for a resource identified by a noun like
    /creator_page, a "get_creator_page" method is looked up in the collection
    object.  The same thing for a PUT request to /45;metadata - a handler named
    "put_metadata" would be looked up in the collection object, and the item
    number would be passed in as the first argument to the method.
    
    LIMITATION: only integer item IDs are supported.
    """
    
    # HTTP methods to 'item' methods - used to determine what Python method to
    # call based on the HTTP method in the request.
    item_dict = {'GET': 'view',
                 'PUT': 'update',
                 'DELETE': 'delete',
                 }
    
    # HTTP methods to 'collection' methods
    col_dict = {'GET': 'list',
                'POST': 'create',
                }
    
    # the item ID/noun specifier separator
    sep = ';'
    
    @cherrypy.expose
    def default(self, *args, **params):
        """The dispatcher."""
        if len(args) > 1:
            # anything beyond a single path element is not handled by this
            # dispatcher.
            raise cherrypy.NotFound
        elif not args:
            item = ''
        else:
            item = args[0]
        
        noun = None
        
        if self.sep in item:
            # immediately split into an item and a noun
            item, noun = item.split(self.sep, 1)
        
        if item.isdigit():
            item = int(item)
        elif not noun:
            # if item isn't a digit and there is no noun, then it must
            # *be* the noun
            noun = item
            item = None
        else:
            raise cherrypy.NotFound
        
        meth = cherrypy.request.method.upper()
        
        # initial handler
        handler_name = "%s_%s" % (meth.lower(), noun)
        
        if not noun:
            # an operation should be performed on the collection
            if item:
                handler_name = self.item_dict.get(meth, '')
            else:
                handler_name = self.col_dict.get(meth, '')
            if not handler_name:
                raise cherrypy.HTTPError(405)
        
        if hasattr(self, handler_name):
            handler = getattr(self, handler_name)
            if item:
                return handler(item, **params)
            else:
                return handler(**params)
        raise cherrypy.NotFound

if __name__ == '__main__':
    from cherrypy.test import helper
    
    def setup_server():
        class TestCollection(Collection):
            def list(self, **params):
                return "All the items"
            
            def create(self, **params):
                return "Created a new item"
            
            def get_creator(self, **params):
                return "An item creator form"
            
            def get_editor(self, item, **params):
                return "An editor form for item %s" % item
            
            def view(self, item, **params):
                return "Viewing item %s" % item
            
            def update(self, item, **params):
                return "Updated item %s with %s" % (item, str(params))
            
            def delete(self, item, **params):
                return "Deleted item %s" % item
            
            def private(self, item, **params):
                return "Did crazy things with %s" % item

        class LimitedCollection(Collection):
            """Only supports GET requests (read-only)"""
            item_dict = {'GET': 'view',
                         }
            
            # HTTP methods to 'collection' methods
            col_dict = {'GET': 'list',
                        }
            
            def list(self, **params):
                return "The list of limited items."
            
            def view(self, item, **params):
                return "Viewing item %s" % item
        
        cherrypy.tree.mount(TestCollection(), '/widgets')
        cherrypy.tree.mount(LimitedCollection(), '/readonly')
        
        cherrypy.config.update({
                'server.log_to_screen': False,
                'autoreload.on': False,
                'log_debug_info_filter.on': False,
        })

    class CollectionTest(helper.CPWebCase):
        def test_list_items(self):
            self.getPage('/widgets/')
            self.assertBody("All the items")

        def test_creator_page(self):
            self.getPage('/widgets/creator')
            self.assertBody("An item creator form")

        def test_create_item(self):
            self.getPage('/widgets/', method="POST")
            self.assertBody('Created a new item')

        def test_item_methods(self):
            expected = {'GET': 'Viewing item 1',
                        'PUT': 'Updated item 1 with {}',
                        'DELETE': 'Deleted item 1',
                        }
            for meth, response in expected.iteritems():
                self.getPage('/widgets/1', method=meth)
                self.assertBody(response)

        def test_item_editor(self):
            self.getPage('/widgets/22;editor')
            self.assertBody('An editor form for item 22')

        def test_405s(self):
            self.getPage('/widgets/10', method='POST')
            self.assertStatus(405)
            self.getPage('/widgets/', method='DELETE')
            self.assertStatus(405)

        def test_404s(self):
            for path in ['/widgets/view', '/widgets/32;foobar',
                         '/widgets/33/editor', '/widgets/fluffy',
                         '/widgets/some/where/out/there']:
                self.getPage(path)
                self.assertStatus(404)

        def test_non_mapped(self):
            """Non-mapped methods should not be available over the web."""
            self.getPage('/widgets/private')
            self.assertStatus(404)
            self.getPage('/widgets/1;private')
            self.assertStatus(404)

        def test_limited(self):
            self.getPage('/readonly/')
            self.assertBody('The list of limited items.')
            
            # try an unimplemented method
            self.getPage('/readonly/', method="POST")
            self.assertStatus(405)

            self.getPage('/readonly/33')
            self.assertBody('Viewing item 33')
            
            # try an unimplemented method
            self.getPage('/readonly/33', method='DELETE')
            self.assertStatus(405)
            
    # start the test
    setup_server()
    helper.testmain()
