| Socialtext-Resting documentation | Contained in the Socialtext-Resting distribution. |
Socialtext::Resting - module for accessing Socialtext REST APIs
use Socialtext::Resting;
my $Rester = Socialtext::Resting->new(
username => $opts{username},
password => $opts{password},
server => $opts{server},
);
$Rester->workspace('wikiname');
$Rester->get_page('my_page');
}
Socialtext::Resting is a module designed to allow remote access
to the Socialtext REST APIs for use in perl programs.
my $Rester = Socialtext::Resting->new(
username => $opts{username},
password => $opts{password},
server => $opts{server},
);
or
my $Rester = Socialtext::Resting->new(
user_cookie => $opts{user_cookie},
server => $opts{server},
);
Creates a Socialtext::Resting object for the specified server/user/password, or server/cookie combination.
$Rester->accept($mime_type);
Sets the HTTP Accept header to ask the server for a specific representation in future requests.
Standard representations: http://www.socialtext.net/st-rest-docs/index.cgi?standard_representations
Common representations:
$Rester->workspace('wikiname');
$Rester->get_page('page_name');
Retrieves the content of the specified page. Note that the workspace method needs to be called first to specify which workspace to operate on.
$Rester->workspace('wikiname');
$Rester->get_attachment('attachment_id');
Retrieves the specified attachment from the workspace. Note that the workspace method needs to be called first to specify which workspace to operate on.
$Rester->workspace('wikiname');
$Rester->put_workspacetag('tag');
Add the specified tag to the workspace.
$Rester->workspace('wikiname');
$Rester->put_pagetag('page_name', 'tag');
Add the specified tag to the page.
$Rester->workspace('wikiname');
$Rester->delete_workspacetag('tag');
Delete the specified tag from the workspace.
$Rester->workspace('wikiname');
$Rester->delete_pagetag('page_name', 'tag');
Delete the specified tag from the page.
$Rester->workspace('wikiname');
$Rester->post_attachment('page_name',$id,$content,$mime_type);
Attach the file to the specified page
$Rester->workspace('wikiname');
$Rester->post_comment( 'page_name', "me too" );
Add a comment to a page.
$Rester->workspace('wikiname');
$Rester->put_page('page_name',$content);
Save the content as a page in the wiki. $content can either be a string, which is treated as wikitext, or a hash with the following keys:
A string which is the page's wiki content.
RFC 2616 HTTP Date format string of the time the page was last edited
A username of the last editor of the page. If the the user does not exist it will be created, but will not be added to the workspace.
my $id = $Rester->name_to_id($name);
my $id = Socialtext::Resting::name_to_id($name);
Convert a page name into a page ID. Can be called as a method or as a function.
$Rester->workspace('wikiname');
$Rester->get_pages();
List all pages in the wiki.
$Rester->get_page_attachments($page)
List all the attachments on a page.
$Rester->get_sheet_cell($page, $cellid)
Get the value of a cell in a spreadsheet.
$Rester->get_revisions($page)
List all the revisions of a page.
$Rester->workspace('wikiname');
$Rester->get_taghistory($page)
Get a history, by revision, of all tags for a page.
Return the page name of the homepage of the current workspace.
$Rester->workspace('wikiname');
$Rester->get_backlinks('page_name');
List all backlinks to the specified page
$Rester->workspace('wikiname');
$Rester->get_frontlinks('page_name');
List all 'frontlinks' on the specified page
$Rester->worksapce('wikiname');
$Rester->get_taggedpages('tag');
List all the pages that are tagged with 'tag'.
$Rester->workspace('wikiname');
$Rester->get_tag('tag');
Retrieves the specified tag from the workspace. Note that the workspace method needs to be called first to specify which workspace to operate on.
$Rester->get_workspace();
Return the metadata about a particular workspace.
$Rester->get_workspaces();
List all workspaces on the server
my $userinfo = $Rester->get_user($username);
print $userinfo->{email_address};
Get information about a username
$Rester->create_user( { username => $username,
email_address => $email,
password => $password } );
Create a new user. Other parameters can be specified, see POD for Socialtext::User. username is optional and will default to the email address, as in most cases username and email_address will be the same.
$Rester->add_user_to_workspace( $workspace, { username => $user,
rolename => $role,
send_confirmation_invitation => 0 || 1,
from_address => $from_email } );
Add a user that already exists to a workspace. rolename defaults to 'member', send_confirmation_invitation defaults to '0'. from_address must refer to a valid existing user, and is only needed if send_confirmation_invitation is set to '1'. If the user is already a member of the workspace, this will reset their role if you specify a role that's different from their current role.
my @users = $Rester->get_users_for_workspace( $workspace );
for ( @users ) { print "$_->{name}, $_->{role}, $->{is_workspace_admin}\n" }
Get a list of users in a workspace, and their roles and admin status.
$Rester->put_persontag( $person, $tag )
Tag a person.
$Rester->get_people();
Retrieves all people.
$Rester->get_person();
Retrieves a person.
$Rester->get_signals();
$Rester->get_signals(group_id => 42);
$Rester->get_signals(account_id => 2);
Retrieves the list of signals.
Optional arguments are passed as query paramaters.
$Rester->post_signal('O HAI');
$Rester->post_signal('O HAI', group_id => 42);
$Rester->post_signal('O HAI', group_ids => [2,3,4]);
$Rester->post_signal('O HAI', account_id => 42);
$Rester->post_signal('O HAI', account_ids => [2,3,4]);
$Rester->post_signal('O HAI', in_reply_to_id => 142);
Posts a signal.
Optional account_ids and group_ids arguments for targetting the signal.
Optional in_reply_to_id for specifying a signal_id this signal is in reply to.
Optional annotations to annotate the signal. annotations should be an array
ref containing hashrefs that have one key (the annotation type) and a value that is
a hashref containing key/value pairs.
$Rester->post_webhook( %args )
Creates a webhook. Args will be encoded as JSON and put up.
my $hooks = $Rester->get_webhooks();
Returns an arrayref containing hashrefs of each webhook on the server.
$Rester->delete_webhook( id => $webhook_id );
Deletes the specified webhook.
my $resp = $Rester->response;
Return the HTTP::Response object from the last request.
Luke Closs <luke.closs@socialtext.com>
Shawn Devlin <shawn.devlin@socialtext.com>
Jeremy Stashewsky <jeremy.stashewsky@socialtext.com>
Chris Dent
Kirsten Jones
Michele Berg - get_revisions()
| Socialtext-Resting documentation | Contained in the Socialtext-Resting distribution. |
package Socialtext::Resting; use strict; use warnings; use URI::Escape; use LWP::UserAgent; use HTTP::Request; use Class::Field 'field'; use JSON::XS; use Readonly; our $VERSION = '0.35';
Readonly my $BASE_URI => '/data'; Readonly my $BASE_WS_URI => $BASE_URI . '/workspaces'; Readonly my %ROUTES => ( backlinks => $BASE_WS_URI . '/:ws/pages/:pname/backlinks', breadcrumbs => $BASE_WS_URI . '/:ws/breadcrumbs', frontlinks => $BASE_WS_URI . '/:ws/pages/:pname/frontlinks', page => $BASE_WS_URI . '/:ws/pages/:pname', pages => $BASE_WS_URI . '/:ws/pages', pagetag => $BASE_WS_URI . '/:ws/pages/:pname/tags/:tag', pagetags => $BASE_WS_URI . '/:ws/pages/:pname/tags', pagetaghistory => $BASE_WS_URI . '/:ws/pages/:pname/taghistory', pagecomments => $BASE_WS_URI . '/:ws/pages/:pname/comments', pageattachment => $BASE_WS_URI . '/:ws/pages/:pname/attachments/:attachment_id', pageattachments => $BASE_WS_URI . '/:ws/pages/:pname/attachments', sheetcells => $BASE_WS_URI . '/:ws/sheets/:pname/cells/:cellid', revisions => $BASE_WS_URI . '/:ws/pages/:pname/revisions', taggedpages => $BASE_WS_URI . '/:ws/tags/:tag/pages', workspace => $BASE_WS_URI . '/:ws', workspaces => $BASE_WS_URI, workspacetag => $BASE_WS_URI . '/:ws/tags/:tag', workspacetags => $BASE_WS_URI . '/:ws/tags', workspaceattachment => $BASE_WS_URI . '/:ws/attachments/:attachment_id', workspaceattachments => $BASE_WS_URI . '/:ws/attachments', workspaceuser => $BASE_WS_URI . '/:ws/users/:user_id', workspaceusers => $BASE_WS_URI . '/:ws/users', user => '/data/users/:user_id', users => '/data/users', homepage => $BASE_WS_URI . '/:ws/homepage', people => $BASE_URI . '/people', person => $BASE_URI . '/people/:pname', person_tag => $BASE_URI . '/people/:pname/tags', profile_photo => $BASE_URI . '/people/:pname/photo', signals => $BASE_URI . '/signals', webhooks => $BASE_URI . '/webhooks', webhook => $BASE_URI . '/webhooks/:id', ); field 'workspace'; field 'username'; field 'password'; field 'user_cookie'; field 'server'; field 'verbose'; field 'accept'; field 'filter'; field 'order'; field 'offset'; field 'count'; field 'query'; field 'etag_cache' => {}; field 'http_header_debug'; field 'response'; field 'json_verbose'; field 'cookie'; field 'agent_string'; field 'on_behalf_of';
sub new { my $invocant = shift; my $class = ref($invocant) || $invocant; my $self = {@_}; return bless $self, $class; }
sub get_page { my $self = shift; my $pname = shift; my $paccept; if (ref $pname){ $paccept = $pname->{accept}; } else { $paccept = $self->accept; } $pname = name_to_id($pname); my $accept = $paccept || 'text/x.socialtext-wiki'; my $workspace = $self->workspace; my $uri = $self->_make_uri( 'page', { pname => $pname, ws => $workspace } ); $uri .= '?verbose=1' if $self->json_verbose; $accept = 'application/json' if $accept eq 'perl_hash'; my ( $status, $content, $response ) = $self->_request( uri => $uri, method => 'GET', accept => $accept, ); if ( $status == 200 || $status == 404 ) { $self->{etag_cache}{$workspace}{$pname} = $response->header('etag'); return decode_json($content) if (($self->accept || '') eq 'perl_hash'); return $content; } else { die "$status: $content\n"; } }
# REVIEW: dup with above, some sub get_attachment { my $self = shift; my $attachment_id = shift; my $uri = $self->_make_uri( 'workspaceattachment', { attachment_id => $attachment_id, ws => $self->workspace, } ); my ( $status, $content ) = $self->_request( uri => $uri, method => 'GET', ); if ( $status == 200 || $status == 404 ) { return $content; } else { die "$status: $content\n"; } }
sub put_workspacetag { my $self = shift; my $tag = shift; my $uri = $self->_make_uri( 'workspacetag', { ws => $self->workspace, tag => $tag } ); my ( $status, $content ) = $self->_request( uri => $uri, method => 'PUT', ); if ( $status == 204 || $status == 201 ) { return $content; } else { die "$status: $content\n"; } }
sub put_pagetag { my $self = shift; my $pname = shift; my $tag = shift; $pname = name_to_id($pname); my $uri = $self->_make_uri( 'pagetag', { pname => $pname, ws => $self->workspace, tag => $tag } ); my ( $status, $content ) = $self->_request( uri => $uri, method => 'PUT', ); if ( $status == 204 || $status == 201 ) { return $content; } else { die "$status: $content\n"; } }
sub delete_workspacetag { my $self = shift; my $tag = shift; my $uri = $self->_make_uri( 'workspacetag', { ws => $self->workspace, tag => $tag } ); my ( $status, $content ) = $self->_request( uri => $uri, method => 'DELETE', ); if ( $status == 204 ) { return $content; } else { die "$status: $content\n"; } }
sub delete_pagetag { my $self = shift; my $pname = shift; my $tag = shift; $pname = name_to_id($pname); my $uri = $self->_make_uri( 'pagetag', { pname => $pname, ws => $self->workspace, tag => $tag } ); my ( $status, $content ) = $self->_request( uri => $uri, method => 'DELETE', ); if ( $status == 204 ) { return $content; } else { die "$status: $content\n"; } }
sub post_attachment { my $self = shift; my $pname = shift; my $attachment_id = shift; my $attachment_content = shift; my $attachment_type = shift; $pname = name_to_id($pname); my $uri = $self->_make_uri( 'pageattachments', { pname => $pname, ws => $self->workspace }, ); $uri .= "?name=$attachment_id"; my ( $status, $content, $response ) = $self->_request( uri => $uri, method => 'POST', type => $attachment_type, content => $attachment_content, ); my $location = $response->header('location'); $location =~ m{.*/attachments/([^/]+)}; $location = URI::Escape::uri_unescape($1); if ( $status == 204 || $status == 201 ) { return $location; } else { die "$status: $content\n"; } }
sub post_comment { my $self = shift; my $pname = shift; my $comment = shift; $pname = name_to_id($pname); my $uri = $self->_make_uri( 'pagecomments', { pname => $pname, ws => $self->workspace }, ); my ( $status, $content ) = $self->_request( uri => $uri, method => 'POST', type => 'text/x.socialtext-wiki', content => $comment, ); die "$status: $content\n" unless $status == 204; }
sub put_page { my $self = shift; my $pname = shift; my $page_content = shift; my $workspace = $self->workspace; my $uri = $self->_make_uri( 'page', { pname => $pname, ws => $workspace } ); my $type = 'text/x.socialtext-wiki'; if ( ref $page_content ) { $type = 'application/json'; $page_content = encode_json($page_content); } my %extra_opts; my $page_id = name_to_id($pname); if (my $prev_etag = $self->{etag_cache}{$workspace}{$page_id}) { $extra_opts{if_match} = $prev_etag; } my ( $status, $content ) = $self->_request( uri => $uri, method => 'PUT', type => $type, content => $page_content, %extra_opts, ); if ( $status == 204 || $status == 201 ) { return $content; } else { die "$status: $content\n"; } } # REVIEW: This is here because of escaping problems we have with # apache web servers. This code effectively translate a Page->uri # to a Page->id. By so doing the troublesome characters are factored # out, getting us past a bug. This change should _not_ be maintained # any longer than strictly necessary, primarily because it # creates an informational dependency between client and server # code by representing name_to_id translation code on both sides # of the system. Since it is not used for page PUT, new pages # will safely have correct page titles. # # This method is useful for clients, so lets make it public. In the # future, this call could go to the server to reduce code duplication.
sub _name_to_id { name_to_id(@_) } sub name_to_id { my $id = shift; $id = shift if ref($id); # handle being called as a method $id = '' if not defined $id; $id =~ s/[^\p{Letter}\p{Number}\p{ConnectorPunctuation}\pM]+/_/g; $id =~ s/_+/_/g; $id =~ s/^_(?=.)//; $id =~ s/(?<=.)_$//; $id =~ s/^0$/_/; $id = lc($id); return $id; } sub _make_uri { my $self = shift; my $thing = shift; my $replacements = shift; my $uri = $ROUTES{$thing}; # REVIEW: tried to do this in on /g go but had issues where # syntax errors were happening... foreach my $stub ( keys(%$replacements) ) { my $replacement = URI::Escape::uri_escape_utf8( $replacements->{$stub} ); $uri =~ s{/:$stub\b}{/$replacement}; } return $uri; }
sub get_pages { my $self = shift; return $self->_get_things('pages'); }
sub get_page_attachments { my $self = shift; my $pname = shift; return $self->_get_things( 'pageattachments', pname => $pname ); }
sub get_sheet_cell { my $self = shift; my $pname = shift; my $cellid = shift; return $self->_get_things('sheetcells', pname => $pname, cellid => $cellid); }
sub get_revisions { my $self = shift; my $pname = shift; return $self->_get_things( 'revisions', pname => $pname ); }
sub get_taghistory { my $self = shift; my $pname = shift; return $self->_get_things( 'pagetaghistory', pname => $pname ); } sub _extend_uri { my $self = shift; my $uri = shift; my @extend; if ( $self->filter ) { push (@extend, "filter=" . $self->filter); } if ( $self->query ) { push (@extend, "q=" . $self->query); } if ( $self->order ) { push (@extend, "order=" . $self->order); } if ( $self->offset ) { push (@extend, "offset=" . $self->offset); } if ( $self->count ) { push (@extend, "count=" . $self->count); } if (@extend) { $uri .= "?" . join(';', @extend); } return $uri; } sub _get_things { my $self = shift; my $things = shift; my %replacements = @_; my $accept = $self->accept || 'text/plain'; my $uri = $self->_make_uri( $things, { ws => $self->workspace, %replacements } ); $uri = $self->_extend_uri($uri); # Add query parameters from a if ( exists $replacements{_query} ) { my @params; for my $q ( keys %{ $replacements{_query} } ) { push @params, "$q=" . $replacements{_query}->{$q}; } if (my $query = join( ';', @params )) { if ( $uri =~ /\?/ ) { $uri .= ";$query"; } else { $uri .= "?$query"; } } } $accept = 'application/json' if $accept eq 'perl_hash'; my ( $status, $content ) = $self->_request( uri => $uri, method => 'GET', accept => $accept, ); if ( $status == 200 and wantarray ) { return ( grep defined, ( split "\n", $content ) ); } elsif ( $status == 200 ) { return decode_json($content) if (($self->accept || '') eq 'perl_hash'); return $content; } elsif ( $status == 404 ) { return (); } elsif ( $status == 302 ) { return $self->response->header('Location'); } else { die "$status: $content\n"; } }
sub get_workspace_tags { my $self = shift; return $self->_get_things( 'workspacetags' ) }
sub get_homepage { my $self = shift; my $uri = $self->_get_things( 'homepage' ); my $workspace = $self->workspace; $uri =~ s#.*/data/workspaces/\Q$workspace\E/pages/(.+)#$1# if $uri; return $uri; }
sub get_backlinks { my $self = shift; my $pname = shift; $pname = name_to_id($pname); return $self->_get_things( 'backlinks', pname => $pname ); }
sub get_frontlinks { my $self = shift; my $pname = shift; my $incipients = shift || 0; $pname = name_to_id($pname); return $self->_get_things( 'frontlinks', pname => $pname, ( $incipients ? ( _query => { incipient => 1 } ) : () ) ); }
sub get_pagetags { my $self = shift; my $pname = shift; $pname = name_to_id($pname); return $self->_get_things( 'pagetags', pname => $pname ); }
sub get_taggedpages { my $self = shift; my $tag = shift; return $self->_get_things( 'taggedpages', tag => $tag ); }
# REVIEW: dup with above, some sub get_tag { my $self = shift; my $tag = shift; my $accept = $self->accept || 'text/html'; my $uri = $self->_make_uri( 'workspacetag', { tag => $tag, ws => $self->workspace, } ); my ( $status, $content ) = $self->_request( uri => $uri, accept => $accept, method => 'GET', ); if ( $status == 200 || $status == 404 ) { return $content; } else { die "$status: $content\n"; } }
sub get_breadcrumbs { my $self = shift; return $self->_get_things('breadcrumbs'); }
sub get_workspace { my $self = shift; my $wksp = shift; my $prev_wksp = $self->workspace(); $self->workspace($wksp) if $wksp; my $result = $self->_get_things('workspace'); $self->workspace($prev_wksp) if $wksp; return $result; }
sub get_workspaces { my $self = shift; return $self->_get_things('workspaces'); }
sub get_user { my $self = shift; my $uname = shift; my $uri = $self->_make_uri( 'user', { user_id => $uname, ws => $self->workspace } ); my ( $status, $content ) = $self->_request( uri => $uri, accept => 'application/json', method => 'GET' ); if ( $status == 200 ) { return decode_json( $content ); } elsif ( $status == 404 ) { return $content; } else { die "$status: $content\n"; } }
sub create_user { my $self = shift; my $args = shift; $args->{ username } ||= $args->{ email_address }; $args = encode_json($args); my ( $status, $content ) = $self->_request( uri => $ROUTES{'users'}, method => 'POST', type => 'application/json', content => $args ); if ( $status == 201 || $status == 400 || $status == 409 ) { return $content; } else { die "$status: $content\n"; } }
sub add_user_to_workspace { my $self = shift; my $workspace = shift; my $args = shift; my $uri = $self->_make_uri( 'workspaceusers', { ws => $workspace } ); $args->{rolename} ||= 'member'; $args->{send_confirmation_invitation} ||= 0; $args = encode_json($args); my ( $status, $content ) = $self->_request( uri => $uri, method => 'POST', type => 'application/json', content => $args ); if ( $status == 201 || $status == 400 ) { return $content; } else { die "$status: $content\n"; } }
sub get_users_for_workspace { my $self = shift; my $workspace = shift; my $uri = $self->_make_uri( 'workspaceusers', { ws => $workspace } ); my ( $status, $content ) = $self->_request( uri => $uri, method => 'GET', accept => 'application/json' ); if ( $status == 200 ) { return @{ decode_json( $content ) }; } else { die "$status: $content\n"; } }
sub put_persontag { my $self = shift; my $person = shift; my $tag = shift; my $uri = $self->_make_uri( 'person_tag', { pname => $person } ); my ( $status, $content ) = $self->_request( uri => $uri, method => 'POST', type => 'application/json', content => encode_json({ tag_name => $tag }), ); return if $status == 200; die "$status: $content\n"; }
sub get_people { my ($self, %opts) = @_; return $self->_get_things('people', _query => \%opts); } sub get_profile_photo { my $self = shift; my $pname = shift; my $uri = $self->_make_uri( 'profile_photo', { pname => $pname }); my ( $status, $content, $response ) = $self->_request( uri => $uri, method => 'GET', ); if ( $status == 200 ) { return $content; } else { die "$status: $content\n"; } }
sub get_person { my $self = shift; my $identifier = shift || $self->username; return $self->_get_things('person', pname => $identifier ); }
sub get_signals { my $self = shift; my %opts = @_; return $self->_get_things('signals', _query => \%opts); }
sub post_signal { my $self = shift; my $text = shift; my %args = @_; my %sig = ( signal => $text ); for my $k (qw(account_id group_id)) { my @ids = @{ $args{$k.'s'} || [] }; push @ids, $args{$k} if $args{$k}; # must be non-zero $sig{$k.'s'} = \@ids if @ids; } for my $k (qw(in_reply_to annotations attachments)) { next unless exists $args{$k}; $sig{$k} = $args{$k}; } my $uri = $self->_make_uri('signals'); my ( $status, $content, $response ) = $self->_request( uri => $uri, method => 'POST', type => "application/json", content => encode_json( \%sig ), ); my $location = $response->header('location'); $location = URI::Escape::uri_unescape($1); if ( $status == 204 || $status == 201 ) { return $location; } else { die "$status: $content\n"; } }
sub post_webhook { my $self = shift; my %args = @_; my $uri = $self->_make_uri('webhooks'); my ( $status, $content, $response ) = $self->_request( uri => $uri, method => 'POST', type => "application/json", content => encode_json( \%args ), ); if ( $status == 204 || $status == 201 ) { return $response->header('Location'); } else { die "$status: $content\n"; } }
sub get_webhooks { my $self = shift; my $uri = $self->_make_uri('webhooks'); my ( $status, $content, $response ) = $self->_request( uri => $uri, method => 'GET', type => "application/json", ); if ( $status == 200 ) { return decode_json($content); } else { die "$status: $content\n"; } }
sub delete_webhook { my $self = shift; my %args = @_; die "id is mandatory" unless $args{id}; my $uri = $self->_make_uri('webhook', {id => $args{id}}); my ( $status, $content, $response ) = $self->_request( uri => $uri, method => 'DELETE', ); if ( $status == 204 ) { return; } else { die "$status: $content\n"; } } sub _request { my $self = shift; my %p = @_; my $ua = LWP::UserAgent->new(agent => $self->agent_string); my $server = $self->server; die "No server defined!\n" unless $server; $server =~ s#/$##; my $uri = "$server$p{uri}"; warn "uri: $uri\n" if $self->verbose; my $request = HTTP::Request->new( $p{method}, $uri ); if ( $self->user_cookie ) { $request->header( 'Cookie' => 'NLW-user=' . $self->user_cookie ); } else { $request->authorization_basic( $self->username, $self->password ); } $request->header( 'Accept' => $p{accept} ) if $p{accept}; $request->header( 'Content-Type' => $p{type} ) if $p{type}; $request->header( 'If-Match' => $p{if_match} ) if $p{if_match}; $request->header( 'X-On-Behalf-Of' => $self->on_behalf_of ) if $self->on_behalf_of; if ($p{method} eq 'PUT') { my $content_len = 0; $content_len = do { use bytes; length $p{content} } if $p{content}; $request->header( 'Content-Length' => $content_len ); } if (my $cookie = $self->cookie) { $request->header('cookie' => $cookie); } $request->content( $p{content} ) if $p{content}; $self->response( $ua->simple_request($request) ); if ( $self->http_header_debug ) { use Data::Dumper; warn "Code: " . $self->response->code . "\n" . Dumper $self->response->headers; } # We should refactor to not return these response things return ( $self->response->code, $self->response->content, $self->response ); }
1;