[Bps-public-commit] rt-extension-rest2 branch, master, updated. d1ff5738adb2ac8b726652600cbe0ff61ee0a993

Shawn Moore shawn at bestpractical.com
Thu Jul 13 18:13:44 EDT 2017


The branch, master has been updated
       via  d1ff5738adb2ac8b726652600cbe0ff61ee0a993 (commit)
       via  6d17ceefaf4babc6282b6fe512e1db891bff663d (commit)
       via  7cf140ec761548b90fad1cb37a7c193818cfd822 (commit)
       via  48848f8f0216ceefc7a253f3af596871ab694858 (commit)
       via  3c719573b98f7a75e76e058a3457b54865432acb (commit)
       via  e7458bfddc7037260f65eb9a256cb1e4cf9806ce (commit)
       via  ca1b6b658da62a508402d571e6f3a9f79a6e52c2 (commit)
       via  239c7ff5b8abfd9341f2c4b25479d4805e0bd8c0 (commit)
      from  d5e268ff64eb3a71a2c61453c23111874e8fd262 (commit)

Summary of changes:
 lib/RT/Extension/REST2.pm                          | 142 ++++++++++++++++++++-
 lib/RT/Extension/REST2/Resource/Queue.pm           |  19 ++-
 lib/RT/Extension/REST2/Resource/Queues.pm          |   9 ++
 .../Extension/REST2/Resource/Record/Hypermedia.pm  |  17 +--
 lib/RT/Extension/REST2/Resource/Record/WithETag.pm |   1 +
 lib/RT/Extension/REST2/Resource/Record/Writable.pm |  16 ++-
 lib/RT/Extension/REST2/Resource/Transactions.pm    |  17 ++-
 lib/RT/Extension/REST2/Util.pm                     |  29 ++++-
 t/queues.t                                         |  18 ++-
 9 files changed, 245 insertions(+), 23 deletions(-)

- Log -----------------------------------------------------------------
commit 239c7ff5b8abfd9341f2c4b25479d4805e0bd8c0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 13 22:05:40 2017 +0000

    Add history hypermedia for queues

diff --git a/lib/RT/Extension/REST2/Resource/Queue.pm b/lib/RT/Extension/REST2/Resource/Queue.pm
index 0d68da5..33af58f 100644
--- a/lib/RT/Extension/REST2/Resource/Queue.pm
+++ b/lib/RT/Extension/REST2/Resource/Queue.pm
@@ -8,7 +8,8 @@ use namespace::autoclean;
 extends 'RT::Extension::REST2::Resource::Record';
 with (
     'RT::Extension::REST2::Resource::Record::Readable',
-    'RT::Extension::REST2::Resource::Record::Hypermedia',
+    'RT::Extension::REST2::Resource::Record::Hypermedia'
+        => { -alias => { hypermedia_links => '_default_hypermedia_links' } },
     'RT::Extension::REST2::Resource::Record::DeletableByDisabling',
     'RT::Extension::REST2::Resource::Record::Writable',
 );
@@ -24,6 +25,16 @@ sub dispatch_rules {
     )
 }
 
+sub hypermedia_links {
+    my $self = shift;
+    my $links = $self->_default_hypermedia_links(@_);
+    my $queue = $self->record;
+
+    push @$links, $self->_transaction_history_link;
+
+    return $links;
+}
+
 __PACKAGE__->meta->make_immutable;
 
 1;
diff --git a/lib/RT/Extension/REST2/Resource/Transactions.pm b/lib/RT/Extension/REST2/Resource/Transactions.pm
index 623839a..2b82c47 100644
--- a/lib/RT/Extension/REST2/Resource/Transactions.pm
+++ b/lib/RT/Extension/REST2/Resource/Transactions.pm
@@ -14,12 +14,21 @@ sub dispatch_rules {
         block => sub { { collection_class => 'RT::Transactions' } },
     ),
     Path::Dispatcher::Rule::Regex->new(
-        regex => qr{^/ticket/(\d+)/history/?$},
+        regex => qr{^/(ticket|queue)/(\d+)/history/?$},
         block => sub {
             my ($match, $req) = @_;
-            my $ticket = RT::Ticket->new($req->env->{"rt.current_user"});
-            $ticket->Load($match->pos(1));
-            return { collection => $ticket->Transactions };
+            my ($class, $id) = ($match->pos(1), $match->pos(2));
+
+            my $record;
+            if ($class eq 'ticket') {
+                $record = RT::Ticket->new($req->env->{"rt.current_user"});
+            }
+            elsif ($class eq 'queue') {
+                $record = RT::Queue->new($req->env->{"rt.current_user"});
+            }
+
+            $record->Load($id);
+            return { collection => $record->Transactions };
         },
     )
 }
diff --git a/t/queues.t b/t/queues.t
index 2517ead..725d503 100644
--- a/t/queues.t
+++ b/t/queues.t
@@ -51,13 +51,16 @@ my $queue_url;
                                      CorrespondAddress CommentAddress);
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
 
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, 1);
     is($links->[0]{type}, 'queue');
     like($links->[0]{_url}, qr[$rest_base_path/queue/1$]);
 
+    is($links->[1]{ref}, 'history');
+    like($links->[1]{_url}, qr[$rest_base_path/queue/1/history$]);
+
     my $creator = $content->{Creator};
     is($creator->{id}, 'RT_System');
     is($creator->{type}, 'user');
@@ -182,13 +185,16 @@ my ($features_url, $features_id);
                                      CorrespondAddress CommentAddress Description);
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 1);
+    is(scalar @$links, 2);
 
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $features_id);
     is($links->[0]{type}, 'queue');
     like($links->[0]{_url}, qr[$rest_base_path/queue/$features_id$]);
 
+    is($links->[1]{ref}, 'history');
+    like($links->[1]{_url}, qr[$rest_base_path/queue/$features_id/history$]);
+
     my $creator = $content->{Creator};
     is($creator->{id}, 'test');
     is($creator->{type}, 'user');

commit ca1b6b658da62a508402d571e6f3a9f79a6e52c2
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 13 22:05:59 2017 +0000

    Add /queues/all endpoint
    
    The /queues endpoint, and other searches, produce no results by default,
    in line with RT's behavior of collections with no ->Limits produce
    no records.

diff --git a/lib/RT/Extension/REST2/Resource/Queues.pm b/lib/RT/Extension/REST2/Resource/Queues.pm
index 69ca02d..0cdd534 100644
--- a/lib/RT/Extension/REST2/Resource/Queues.pm
+++ b/lib/RT/Extension/REST2/Resource/Queues.pm
@@ -13,6 +13,15 @@ sub dispatch_rules {
         regex => qr{^/queues/?$},
         block => sub { { collection_class => 'RT::Queues' } },
     ),
+    Path::Dispatcher::Rule::Regex->new(
+        regex => qr{^/queues/all/?$},
+        block => sub {
+            my ($match, $req) = @_;
+            my $queues = RT::Queues->new($req->env->{"rt.current_user"});
+            $queues->UnLimit;
+            return { collection => $queues };
+        },
+    ),
 }
 
 __PACKAGE__->meta->make_immutable;

commit e7458bfddc7037260f65eb9a256cb1e4cf9806ce
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 13 22:07:46 2017 +0000

    Factor out a custom_fields_for function
    
    The hypermedia role wasn't checking RT::CustomField->LookupTypes

diff --git a/lib/RT/Extension/REST2/Resource/Record/Hypermedia.pm b/lib/RT/Extension/REST2/Resource/Record/Hypermedia.pm
index 3af34e0..9c6dfdc 100644
--- a/lib/RT/Extension/REST2/Resource/Record/Hypermedia.pm
+++ b/lib/RT/Extension/REST2/Resource/Record/Hypermedia.pm
@@ -4,7 +4,7 @@ use warnings;
 
 use Moose::Role;
 use namespace::autoclean;
-use RT::Extension::REST2::Util qw(expand_uid);
+use RT::Extension::REST2::Util qw(expand_uid custom_fields_for);
 use JSON qw(to_json);
 
 sub hypermedia_links {
@@ -77,13 +77,14 @@ sub _customfield_links {
     my $record = $self->record;
     my @links;
 
-    my $cfs = $record->CustomFields;
-    while (my $cf = $cfs->Next) {
-        my $entry = expand_uid($cf->UID);
-        push @links, {
-            %$entry,
-            ref => 'customfield',
-        };
+    if (my $cfs = custom_fields_for($record)) {
+        while (my $cf = $cfs->Next) {
+            my $entry = expand_uid($cf->UID);
+            push @links, {
+                %$entry,
+                ref => 'customfield',
+            };
+        }
     }
 
     return @links;
diff --git a/lib/RT/Extension/REST2/Util.pm b/lib/RT/Extension/REST2/Util.pm
index 2a4f4ce..5236dff 100644
--- a/lib/RT/Extension/REST2/Util.pm
+++ b/lib/RT/Extension/REST2/Util.pm
@@ -16,6 +16,7 @@ use Sub::Exporter -setup => {
         record_class
         escape_uri
         query_string
+        custom_fields_for
     ]]
 };
 
@@ -92,12 +93,8 @@ sub serialize_record {
         }
     }
 
-    # Custom fields; no role yet, but we have registered lookup types
-    my %registered_type = map {; $_ => 1 } RT::CustomField->LookupTypes;
-    if ($registered_type{$record->CustomFieldLookupType}) {
+    if (my $cfs = custom_fields_for($record)) {
         my %values;
-
-        my $cfs = $record->CustomFields;
         while (my $cf = $cfs->Next) {
             my $key    = $cf->Id;
             my $values = $values{$key} ||= [];
@@ -209,4 +206,16 @@ sub query_string {
     return join '&', @params;
 }
 
+sub custom_fields_for {
+    my $record = shift;
+
+    # no role yet, but we have registered lookup types
+    my %registered_type = map {; $_ => 1 } RT::CustomField->LookupTypes;
+    if ($registered_type{$record->CustomFieldLookupType}) {
+        return $record->CustomFields;
+    }
+
+    return;
+}
+
 1;

commit 3c719573b98f7a75e76e058a3457b54865432acb
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 13 22:08:55 2017 +0000

    Avoid error trying to produce txn->CustomFields for non-Ticket records

diff --git a/lib/RT/Extension/REST2/Util.pm b/lib/RT/Extension/REST2/Util.pm
index 5236dff..e0998d6 100644
--- a/lib/RT/Extension/REST2/Util.pm
+++ b/lib/RT/Extension/REST2/Util.pm
@@ -212,7 +212,17 @@ sub custom_fields_for {
     # no role yet, but we have registered lookup types
     my %registered_type = map {; $_ => 1 } RT::CustomField->LookupTypes;
     if ($registered_type{$record->CustomFieldLookupType}) {
-        return $record->CustomFields;
+        # see $HasTxnCFs in /Elements/ShowHistoryPage; seems like it's working
+        # around a bug in RT::Transaction->CustomFieldLookupId
+        if ($record->isa('RT::Transaction')) {
+            my $object = $record->Object;
+            if ($object->can('TransactionCustomFields') && $object->TransactionCustomFields->Count) {
+                return $object->TransactionCustomFields;
+            }
+        }
+        else {
+            return $record->CustomFields;
+        }
     }
 
     return;

commit 48848f8f0216ceefc7a253f3af596871ab694858
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 13 22:09:19 2017 +0000

    Only provide Last-Modified if the record type supports it
    
    RT::Transaction does not have LastUpdated

diff --git a/lib/RT/Extension/REST2/Resource/Record/WithETag.pm b/lib/RT/Extension/REST2/Resource/Record/WithETag.pm
index a52aa9d..9f05ad3 100644
--- a/lib/RT/Extension/REST2/Resource/Record/WithETag.pm
+++ b/lib/RT/Extension/REST2/Resource/Record/WithETag.pm
@@ -9,6 +9,7 @@ requires 'record';
 
 sub last_modified {
     my $self = shift;
+    return unless $self->record->_Accessible("LastUpdated" => "read");
     return $self->record->LastUpdatedObj->W3CDTF( Timezone => 'UTC' );
 }
 

commit 7cf140ec761548b90fad1cb37a7c193818cfd822
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 13 22:12:07 2017 +0000

    Provide the usual UID-type response body when you create a record
    
    The URL has always been provided in the Location header, but this makes
    it more accessible, and gives you the id too

diff --git a/lib/RT/Extension/REST2/Resource/Record/Writable.pm b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
index 73b18ae..f30033b 100644
--- a/lib/RT/Extension/REST2/Resource/Record/Writable.pm
+++ b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
@@ -5,7 +5,7 @@ use warnings;
 use Moose::Role;
 use namespace::autoclean;
 use JSON ();
-use RT::Extension::REST2::Util qw( deserialize_record error_as_json );
+use RT::Extension::REST2::Util qw( deserialize_record error_as_json expand_uid );
 use List::MoreUtils 'uniq';
 
 with 'RT::Extension::REST2::Resource::Role::RequestBodyIsJSON'
@@ -281,6 +281,11 @@ sub create_resource {
 
     my ($ok, $msg) = $self->create_record($data);
     if ($ok) {
+        my $response = $self->response;
+        my $body = JSON::encode_json(expand_uid($self->record->UID));
+        $response->content_type( "application/json; charset=utf-8" );
+        $response->content_length( length $body );
+        $response->body( $body );
         return;
     } else {
         return error_as_json(

commit 6d17ceefaf4babc6282b6fe512e1db891bff663d
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 13 22:12:33 2017 +0000

    Allow specifying some POST/PUT parameters in URL
    
    This allows more and better hypermedia links, such as (also in this
    queue) linking from a queue to creating a ticket in that queue

diff --git a/lib/RT/Extension/REST2/Resource/Queue.pm b/lib/RT/Extension/REST2/Resource/Queue.pm
index 33af58f..af27783 100644
--- a/lib/RT/Extension/REST2/Resource/Queue.pm
+++ b/lib/RT/Extension/REST2/Resource/Queue.pm
@@ -32,6 +32,12 @@ sub hypermedia_links {
 
     push @$links, $self->_transaction_history_link;
 
+    push @$links, {
+        ref  => 'create',
+        type => 'ticket',
+        _url => RT::Extension::REST2->base_uri . '/ticket?Queue=' . $queue->Id,
+    } if $queue->CurrentUserHasRight('CreateTicket');
+
     return $links;
 }
 
diff --git a/lib/RT/Extension/REST2/Resource/Record/Writable.pm b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
index f30033b..eedbfe8 100644
--- a/lib/RT/Extension/REST2/Resource/Record/Writable.pm
+++ b/lib/RT/Extension/REST2/Resource/Record/Writable.pm
@@ -25,9 +25,16 @@ sub content_types_accepted { [ {'application/json' => 'from_json'} ] }
 
 sub from_json {
     my $self = shift;
+    my $params = JSON::decode_json( $self->request->content );
+
+    %$params = (
+        %$params,
+        %{ $self->request->query_parameters->mixed },
+    );
+
     my $data = deserialize_record(
         $self->record,
-        JSON::decode_json( $self->request->content ),
+        $params,
     );
 
     my $method = $self->request->method;
diff --git a/t/queues.t b/t/queues.t
index 725d503..2888383 100644
--- a/t/queues.t
+++ b/t/queues.t
@@ -51,7 +51,7 @@ my $queue_url;
                                      CorrespondAddress CommentAddress);
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 2);
+    is(scalar @$links, 3);
 
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, 1);
@@ -61,6 +61,10 @@ my $queue_url;
     is($links->[1]{ref}, 'history');
     like($links->[1]{_url}, qr[$rest_base_path/queue/1/history$]);
 
+    is($links->[2]{ref}, 'create');
+    is($links->[2]{type}, 'ticket');
+    like($links->[2]{_url}, qr[$rest_base_path/ticket\?Queue=1$]);
+
     my $creator = $content->{Creator};
     is($creator->{id}, 'RT_System');
     is($creator->{type}, 'user');
@@ -185,7 +189,7 @@ my ($features_url, $features_id);
                                      CorrespondAddress CommentAddress Description);
 
     my $links = $content->{_hyperlinks};
-    is(scalar @$links, 2);
+    is(scalar @$links, 3);
 
     is($links->[0]{ref}, 'self');
     is($links->[0]{id}, $features_id);
@@ -195,6 +199,10 @@ my ($features_url, $features_id);
     is($links->[1]{ref}, 'history');
     like($links->[1]{_url}, qr[$rest_base_path/queue/$features_id/history$]);
 
+    is($links->[2]{ref}, 'create');
+    is($links->[2]{type}, 'ticket');
+    like($links->[2]{_url}, qr[$rest_base_path/ticket\?Queue=$features_id$]);
+
     my $creator = $content->{Creator};
     is($creator->{id}, 'test');
     is($creator->{type}, 'user');

commit d1ff5738adb2ac8b726652600cbe0ff61ee0a993
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Thu Jul 13 22:13:27 2017 +0000

    Tutorial documentation

diff --git a/lib/RT/Extension/REST2.pm b/lib/RT/Extension/REST2.pm
index c923e26..c0ff7c3 100644
--- a/lib/RT/Extension/REST2.pm
+++ b/lib/RT/Extension/REST2.pm
@@ -44,7 +44,147 @@ Add this line:
 
 =head1 USAGE
 
-=head2 Summary
+=head2 Tutorial
+
+To make it easier to authenticate to REST2, we recommend installing
+L<RT::Authen::Token>. Visit "Logged in as ___" -> Settings -> Auth
+Tokens. Create an Auth Token, give it any description (such as "REST2
+with curl"). Make note of the authentication token it provides to you.
+
+=head3 Authentication
+
+Run the following in a terminal, filling in XX_TOKEN_XX from the auth
+token above and XX_RT_URL_XX with the URL for your RT instance.
+
+    curl -H 'Authorization: token XX_TOKEN_XX' 'XX_RT_URL_XX/REST/2.0/queues/all'
+
+This does an authenticated request (using the C<Authorization> HTTP
+header) for all of the queues you can see. You should see a response,
+typical of search results, like this:
+
+    {
+       "total" : 1,
+       "count" : 1,
+       "page" : 1,
+       "per_page" : 20,
+       "items" : [
+          {
+             "type" : "queue",
+             "id" : "1",
+             "_url" : "XX_RT_URL_XX/REST/2.0/queue/1"
+          }
+       ]
+    }
+
+(If you instead see a response like C<{"message":"Unauthorized"}> that
+indicates RT couldn't process your authentication token successfully;
+make sure the word "token" appears between "Authorization:" and the auth
+token that RT provided to you)
+
+=head3 Following Links
+
+You can request the provided C<_url> to get more information about that
+resource.
+
+    curl -H 'Authorization: token XX_TOKEN_XX' 'XX_RT_URL_XX/REST/2.0/queue/1'
+
+This will give a lot of information about that queue, such as its Name,
+Description, Creator, and so on.
+
+Of particular note is the C<_hyperlinks> key, which gives you additional
+resources to examine (following the
+L<https://en.wikipedia.org/wiki/HATEOAS> principle). For example an
+entry with a C<ref> of C<history> lets you examine the transaction log
+for a record. You can implement your REST API client knowing that any
+other hypermedia link with a C<ref> of C<history> has the same meaning,
+regardless of whether it's the history of a queue, ticket, asset, etc.
+
+Another C<ref> you'll see in C<_hyperlinks> is C<create>, with a C<type>
+of C<ticket>. This of course gives you the URL to create tickets I<in
+this queue>. Importantly, if you did I<not> have the C<CreateTicket>
+permission in this queue, then REST2 would not include this hyperlink in
+its response to your request. So you can dynamically adapt your client's
+behavior to its presence or absence, just like the web version of RT
+does.
+
+=head3 Creating Tickets
+
+To create a ticket is a bit more involved, since it requires providing a
+different HTTP verb (C<POST> instead of C<GET>), a C<Content-Type>
+header (to tell REST2 that your content is JSON instead of, say, XML),
+and the fields for your new ticket such as Subject. Here is the curl
+invocation, wrapped to multiple lines for readability.
+
+    curl -X POST
+         -H "Content-Type: application/json"
+         -d '{ "Subject": "hello world" }'
+         -H 'Authorization: token XX_TOKEN_XX'
+            'XX_RT_URL_XX/REST/2.0/ticket?Queue=1'
+
+That will give us the URL of the new ticket, and so we can fetch that
+URL to continue working with this ticket. Request the ticket like so
+(make sure to include the C<-i> flag to see response's HTTP
+headers).
+
+    curl -i -H 'Authorization: token XX_TOKEN_XX' 'TICKET_URL'
+
+You'll first see that there are many hyperlinks for tickets, including
+one for each Lifecycle action you can perform. Additionally you'll see
+an C<ETag> header for this record, which can be used for conflict
+avoidance (L<https://en.wikipedia.org/wiki/HTTP_ETag>). Let's try
+updating this ticket with an I<invalid> C<ETag> to see what happens.
+
+=head3 Updating Tickets
+
+    curl -X PUT
+         -H "Content-Type: application/json"
+         -H "If-Match: invalid-etag"
+         -d '{ "Subject": "cannot update" }'
+         -H 'Authorization: token XX_TOKEN_XX'
+            'TICKET_URL'
+
+You'll get an error response like C<{"message":"Precondition Failed"}>
+and a status code of 412. If you examine the ticket, you'll also see
+that its Subject was not changed. This is because the C<If-Match> header
+advises the server to make changes I<if and only if> the ticket's
+C<ETag> matches what you provide. Since it differed, the server rejected
+the request.
+
+Now, try the same request by replacing the value "invalid-etag" with
+the real C<ETag> you received when you requested the ticket previously.
+You'll then get a JSON response like:
+
+    ["Ticket 1: Subject changed from 'hello world' to 'cannot update'"]
+
+And if you C<GET> the ticket again, you'll observe that the C<ETag>
+header now has a different value, indicating that it has changed. This
+means if you were to retry the C<PUT> update with the previous (at the
+time, expected) C<ETag> you would instead be rejected by the server with
+Precondition Failed. With these tools you can use C<ETag> and
+C<If-Match> headers to avoid race conditions such as two people updating
+a ticket at the same time. Depending on the sophistication of your
+client,
+
+You may of course choose to ignore the C<ETag> header and not provide
+C<If-Match> in your requests; RT doesn't require its use.
+
+=head3 Summary
+
+RT's REST2 API provides the tools you need to build robust and dynamic
+integrations. Tools like C<ETag>/C<If-Match> allow you to avoid
+conflicts such as two people taking a ticket at the same time. Using
+JSON for all data interchange avoids problems caused by parsing text.
+Hypermedia links inform your client application of what the user has the
+ability to do.
+
+Careful readers will see that, other than our initial entry into the
+system, we did not I<generate> any URLs. We only I<followed> URLs, just
+like you do when browsing a website on your computer. We've better
+decoupled the client's implementation from the server's REST API.
+Additionally, this system lets you be informed of new capabilities in
+the form of additional hyperlinks.
+
+=head2 Endpoints
 
 Currently provided endpoints under C</REST/2.0/> are:
 

-----------------------------------------------------------------------


More information about the Bps-public-commit mailing list