Index: trunk/BookmarkController.pm
===================================================================
--- trunk/BookmarkController.pm	(revision 67)
+++ trunk/BookmarkController.pm	(revision 68)
@@ -43,66 +43,4 @@
     $url .= '/' unless $url =~ m{/$};
     return URI->new($url);
-}
-
-sub _get_list_links {
-    my $self = shift;
-    my ($self_type, $query) = @_;
-    my @links = (
-        {
-            text => 'JSON',
-            type => 'application/json',
-            query => {
-                %$query,
-                format => 'json',
-            },
-        },
-        {
-            text => 'XBEL',
-            type => 'application/xml',
-            query => {
-                %$query,
-                format => 'xbel',
-            },
-        },
-        {
-            text => 'Atom',
-            type => 'application/atom+xml',
-            path => 'feed',
-            query => {
-                %$query,
-            },
-        },
-        {
-            text => 'CSV',
-            type => 'text/csv',
-            query => {
-                %$query,
-                format => 'csv',
-            },
-        },
-        {
-            text => 'URI List',
-            type => 'text/uri-list',
-            query => {
-                %$query,
-                format => 'text',
-            },
-        },
-        {
-            text => 'HTML',
-            type => 'text/html',
-            query => {
-                %$query,
-            },
-        },
-    );
-
-    for my $link (@links) {
-        $link->{rel}  = $link->{type} eq $self_type ? 'self' : 'alternate';
-        $link->{href} = URI->new_abs($link->{path} || '', $self->base_uri);
-        $link->{href}->query_form($link->{query});
-    }
-
-    return @links;
 }
 
@@ -143,5 +81,6 @@
     my $limit = $self->request->param('limit');
     my $offset = $self->request->param('offset');
-    my @resources = $self->bookmarks->get_bookmarks({
+
+    my $list = $self->bookmarks->get_bookmarks({
         query  => $query,
         tag    => \@tags,
@@ -149,102 +88,10 @@
         offset => $offset,
     });
-    my @all_tags = $self->bookmarks->get_tags({ selected => $tags[0] });
-    my @cotags = $self->bookmarks->get_cotags({
-        query  => $query,
-        tag    => \@tags,
-    });
-    
-    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '') . ($query ? " matching '$query'" : '');
-
-    if ($format eq 'json') {
-        my $json = decode_utf8(
-            JSON->new->utf8->convert_blessed->encode({
-                bookmarks => \@resources,
-            })
-        );
-        return [200, ['Content-Type' => 'application/json; charset=UTF-8'], [$json]];
-    } elsif ($format eq 'xbel') {
-        require XML::XBEL;
-        #TODO: conditional support; if XML::XBEL is not present, return a 5xx response
-
-        my $xbel = XML::XBEL->new;
-
-        $xbel->new_document({
-            title => $title,
-        });
-
-        for my $bookmark (@resources) {
-            my $cdatetime = time2isoz $bookmark->ctime;
-            my $mdatetime = time2isoz $bookmark->mtime;
-            # make the timestamps W3C-correct
-            s/ /T/ foreach ($cdatetime, $mdatetime);
-
-            $xbel->add_bookmark({
-                href     => $bookmark->uri,
-                title    => $bookmark->title,
-                desc     => 'Tags: ' . join(', ', @{ $bookmark->tags }),
-                added    => $cdatetime,
-                #XXX: are we sure that modified is the mtime of the bookmark or the resource?
-                modified => $mdatetime,
-            });
-        }
-
-        return [200, ['Content-Type' => 'application/xml; charset=UTF-8'], [$xbel->toString]];
-    } elsif ($format eq 'text') {
-        my $text = join '', 
-            map {
-                sprintf "# %s\n# Tags: %s\n%s\n",
-                $_->title,
-                join(', ', @{ $_->tags }), 
-                $_->uri
-            } @resources;
-        return [200, ['Content-Type' => 'text/uri-list; charset=UTF-8'], [$text]];
-    } elsif ($format eq 'csv') {
-        require Text::CSV::Encoded;
-        my $csv = Text::CSV::Encoded->new({ encoding_out => 'utf8' });
-        my $text = qq{id,uri,title,tags,ctime,mtime\n};
-        for my $bookmark (@resources) {
-            my $success = $csv->combine(
-                $bookmark->id,
-                $bookmark->uri,
-                $bookmark->title,
-                join(' ', @{ $bookmark->tags }),
-                $bookmark->ctime,
-                $bookmark->mtime,
-            );
-            $text .= $csv->string . "\n" if $success;
-        }
-
-        # include the local timestamp in the attchment filename
-        my $dt = time2iso;
-        $dt =~ s/[^\d]//g;
-
-        my $filename = sprintf(
-            'bookmarks-%s-%s.csv',
-            join('_', @tags),
-            $dt,
-        );
-
-        return [200, ['Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => sprintf('attachement; filename="%s"', $filename)], [$text]];
-    } else {
-        my $template = Template->new;
-
-        $template->process(
-            'list.tt',
-            {
-                base_url     => $self->base_uri,
-                title        => $title,
-                query        => $query,
-                selected_tag => $tags[0],
-                search_tags  => \@tags,
-                links        => [ $self->_get_list_links('text/html', { q => $query, tag => \@tags }) ],
-                all_tags     => \@all_tags,
-                cotags       => \@cotags,
-                resources    => \@resources,
-            },
-            \my $output,
-        );
-        return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
-    }
+
+    my $as_format = "as_$format";
+    if (!$list->meta->has_method($as_format)) {
+        return [406, ['Content-Type' => 'text/plain; charset=UTF-8'], [qq{"$format" is not a supported format}]];
+    }
+    return $list->$as_format;
 }
 
@@ -254,54 +101,8 @@
     my $query = $self->request->param('q');
     my @tags = grep { $_ ne '' } $self->request->param('tag');
-    my $title = 'Bookmarks' . (@tags ? " tagged as " . join(' & ', @tags) : '');
-
-    require XML::Atom;
-    $XML::Atom::DefaultVersion = "1.0";
-
-    require XML::Atom::Feed;
-    require XML::Atom::Entry;
-    require XML::Atom::Link;
-    require XML::Atom::Category;
-
-    my $feed = XML::Atom::Feed->new;
-    $feed->title($title);
-
-    for my $link ($self->_get_list_links('application/atom+xml', { q => $query, tag => \@tags })) {
-        my $atom_link = XML::Atom::Link->new;
-        $atom_link->type($link->{type});
-        $atom_link->rel($link->{rel});
-        $atom_link->href($link->{href}->canonical);
-        $feed->add_link($atom_link);
-    }
 
     # construct a feed from the most recent 12 bookmarks
-    for my $bookmark ($self->bookmarks->get_bookmarks({ query => $query, tag => \@tags, limit => 12 })) {
-        my $entry = XML::Atom::Entry->new;
-        $entry->id($bookmark->bookmark_uri->canonical);
-        $entry->title($bookmark->title);
-        
-        my $link = XML::Atom::Link->new;
-        $link->href($bookmark->uri);
-        $entry->add_link($link);
-        
-        $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags }));
-
-        my $cdatetime = time2isoz $bookmark->ctime;
-        my $mdatetime = time2isoz $bookmark->mtime;
-        # make the timestamp W3C-correct
-        s/ /T/ foreach ($cdatetime, $mdatetime);
-        $entry->published($cdatetime);
-        $entry->updated($mdatetime);
-        
-        for my $tag (@{ $bookmark->tags }) {
-            my $category = XML::Atom::Category->new;
-            $category->term($tag);
-            $entry->add_category($category);
-        }
-
-        $feed->add_entry($entry);
-    }
-
-    return [200, ['Content-Type' => 'application/atom+xml; charset=UTF-8'], [$feed->as_xml]];
+    my $list = $self->bookmarks->get_bookmarks({ query => $query, tag => \@tags, limit => 12 });
+    return $list->as_atom;
 }
 
Index: trunk/Bookmarks.pm
===================================================================
--- trunk/Bookmarks.pm	(revision 67)
+++ trunk/Bookmarks.pm	(revision 68)
@@ -5,4 +5,5 @@
 use URI;
 use Bookmark;
+use BookmarksList;
 
 has dbh      => ( is => 'rw' );
@@ -116,5 +117,12 @@
         });
     }
-    return @resources;
+    return BookmarksList->new({
+        bookmarks => $self,
+        tags      => $tags,
+        query     => $query,
+        limit     => $limit,
+        offset    => $offset,
+        results   => \@resources,
+    });
 }
 
Index: trunk/BookmarksList.pm
===================================================================
--- trunk/BookmarksList.pm	(revision 68)
+++ trunk/BookmarksList.pm	(revision 68)
@@ -0,0 +1,255 @@
+package BookmarksList;
+
+use Moose;
+
+use Encode;
+use HTTP::Date qw{time2iso time2isoz};
+
+has bookmarks => (is => 'ro');
+has query  => (is => 'ro');
+has tags   => (
+    is => 'ro',
+    default => sub { [] },
+);
+has limit  => (is => 'ro');
+has offset => (is => 'ro');
+has results => ( is => 'ro' );
+has title => (
+    is => 'ro',
+    builder => '_build_title',
+    lazy => 1,
+);
+
+sub _get_list_links {
+    my $self = shift;
+    my ($self_type, $query) = @_;
+    my @links = (
+        {
+            text => 'JSON',
+            type => 'application/json',
+            query => {
+                %$query,
+                format => 'json',
+            },
+        },
+        {
+            text => 'XBEL',
+            type => 'application/xml',
+            query => {
+                %$query,
+                format => 'xbel',
+            },
+        },
+        {
+            text => 'Atom',
+            type => 'application/atom+xml',
+            path => 'feed',
+            query => {
+                %$query,
+            },
+        },
+        {
+            text => 'CSV',
+            type => 'text/csv',
+            query => {
+                %$query,
+                format => 'csv',
+            },
+        },
+        {
+            text => 'URI List',
+            type => 'text/uri-list',
+            query => {
+                %$query,
+                format => 'text',
+            },
+        },
+        {
+            text => 'HTML',
+            type => 'text/html',
+            query => {
+                %$query,
+            },
+        },
+    );
+
+    for my $link (@links) {
+        $link->{rel}  = $link->{type} eq $self_type ? 'self' : 'alternate';
+        $link->{href} = URI->new_abs($link->{path} || '', $self->bookmarks->base_uri);
+        $link->{href}->query_form($link->{query});
+    }
+
+    return @links;
+}
+
+sub _build_title {
+    my $self = shift;
+    return 'Bookmarks' . (@{ $self->tags } ? " tagged as " . join(' & ', @{ $self->tags }) : '') . ($self->query ? " matching '" . $self->query . "'" : '');
+}
+
+sub as_json {
+    my $self = shift;
+    require JSON;
+    my $json = decode_utf8(
+        JSON->new->utf8->convert_blessed->encode({
+            bookmarks => $self->results,
+        })
+    );
+    return [200, ['Content-Type' => 'application/json; charset=UTF-8'], [$json]];
+}
+
+sub as_xbel {
+    my $self = shift;
+    require XML::XBEL;
+    #TODO: conditional support; if XML::XBEL is not present, return a 5xx response
+
+    my $xbel = XML::XBEL->new;
+
+    $xbel->new_document({
+        title => $self->title,
+    });
+
+    for my $bookmark (@{ $self->results }) {
+        my $cdatetime = time2isoz $bookmark->ctime;
+        my $mdatetime = time2isoz $bookmark->mtime;
+        # make the timestamps W3C-correct
+        s/ /T/ foreach ($cdatetime, $mdatetime);
+
+        $xbel->add_bookmark({
+            href     => $bookmark->uri,
+            title    => $bookmark->title,
+            desc     => 'Tags: ' . join(', ', @{ $bookmark->tags }),
+            added    => $cdatetime,
+            #XXX: are we sure that modified is the mtime of the bookmark or the resource?
+            modified => $mdatetime,
+        });
+    }
+
+    return [200, ['Content-Type' => 'application/xml; charset=UTF-8'], [$xbel->toString]];
+}
+
+sub as_text {
+    my $self = shift;
+    my $text = join '', 
+        map {
+            sprintf "# %s\n# Tags: %s\n%s\n",
+            $_->title,
+            join(', ', @{ $_->tags }), 
+            $_->uri
+        } @{ $self->results };
+    return [200, ['Content-Type' => 'text/uri-list; charset=UTF-8'], [$text]];
+}
+
+sub as_csv {
+    my $self = shift;
+    require Text::CSV::Encoded;
+    my $csv = Text::CSV::Encoded->new({ encoding_out => 'utf8' });
+    my $text = qq{id,uri,title,tags,ctime,mtime\n};
+    for my $bookmark (@{ $self->results }) {
+        my $success = $csv->combine(
+            $bookmark->id,
+            $bookmark->uri,
+            $bookmark->title,
+            join(' ', @{ $bookmark->tags }),
+            $bookmark->ctime,
+            $bookmark->mtime,
+        );
+        $text .= $csv->string . "\n" if $success;
+    }
+
+    # include the local timestamp in the attchment filename
+    my $dt = time2iso;
+    $dt =~ s/[^\d]//g;
+
+    my $filename = sprintf(
+        'bookmarks-%s-%s.csv',
+        join('_', @{ $self->tags }),
+        $dt,
+    );
+
+    return [200, ['Content-Type' => 'text/csv; charset=UTF-8', 'Content-Disposition' => sprintf('attachement; filename="%s"', $filename)], [$text]];
+}
+
+sub as_html {
+    my $self = shift;
+
+    require Template;
+    my $template = Template->new;
+
+    my @all_tags = $self->bookmarks->get_tags({ selected => @{ $self->tags }[0] });
+    my @cotags = $self->bookmarks->get_cotags({
+        query  => $self->query,
+        tag    => $self->tags,
+    });
+
+    $template->process(
+        'list.tt',
+        {
+            base_url     => $self->bookmarks->base_uri,
+            title        => $self->title,
+            query        => $self->query,
+            selected_tag => @{ $self->tags }[0],
+            search_tags  => $self->tags,
+            links        => [ $self->_get_list_links('text/html', { q => $self->query, tag => $self->tags }) ],
+            all_tags     => \@all_tags,
+            cotags       => \@cotags,
+            resources    => $self->results,
+        },
+        \my $output,
+    );
+    return [200, ['Content-Type' => 'text/html; charset=UTF-8'], [$output]];
+}
+
+sub as_atom {
+    my $self = shift;
+
+    require XML::Atom;
+    $XML::Atom::DefaultVersion = "1.0";
+
+    require XML::Atom::Feed;
+    require XML::Atom::Entry;
+    require XML::Atom::Link;
+    require XML::Atom::Category;
+
+    my $feed = XML::Atom::Feed->new;
+    $feed->title($self->title);
+
+    for my $link ($self->_get_list_links('application/atom+xml', { q => $self->query, tag => $self->tags })) {
+        my $atom_link = XML::Atom::Link->new;
+        $atom_link->type($link->{type});
+        $atom_link->rel($link->{rel});
+        $atom_link->href($link->{href}->canonical);
+        $feed->add_link($atom_link);
+    }
+
+    for my $bookmark (@{ $self->results }) {
+        my $entry = XML::Atom::Entry->new;
+        $entry->id($bookmark->bookmark_uri->canonical);
+        $entry->title($bookmark->title);
+        
+        my $link = XML::Atom::Link->new;
+        $link->href($bookmark->uri);
+        $entry->add_link($link);
+        
+        $entry->summary('Tags: ' . join(', ', @{ $bookmark->tags }));
+
+        my $cdatetime = time2isoz $bookmark->ctime;
+        my $mdatetime = time2isoz $bookmark->mtime;
+        # make the timestamp W3C-correct
+        s/ /T/ foreach ($cdatetime, $mdatetime);
+        $entry->published($cdatetime);
+        $entry->updated($mdatetime);
+        
+        for my $tag (@{ $bookmark->tags }) {
+            my $category = XML::Atom::Category->new;
+            $category->term($tag);
+            $entry->add_category($category);
+        }
+
+        $feed->add_entry($entry);
+    }
+
+    return [200, ['Content-Type' => 'application/atom+xml; charset=UTF-8'], [$feed->as_xml]];
+}
+
+1;
