First working query example with promise based backend

Change-Id: Iee7360d123dc09876d942a32013077d78ea50b91
diff --git a/lib/Kalamar/Controller/Search2.pm b/lib/Kalamar/Controller/Search2.pm
index 2cdff6c..e018fec 100644
--- a/lib/Kalamar/Controller/Search2.pm
+++ b/lib/Kalamar/Controller/Search2.pm
@@ -1,13 +1,193 @@
 package Kalamar::Controller::Search2;
 use Mojo::Base 'Mojolicious::Controller';
+use Data::Dumper;
+use Mojo::Collection 'c';
+use Mojo::ByteStream 'b';
 
-has api => 'http://10.0.10.52:9000/api/';
+# This should be implemented as a helper
+has api => '/api/';
 
 has no_cache => 0;
 
 has items_per_page => 25;
 
 
+# Catch connection errors
+sub _catch_http_errors {
+  my $tx = shift;
+  my $err = $tx->error;
+
+  if ($err) {
+    return Mojo::Promise->new->reject([
+      [$err->{code}, $err->{message}]
+    ]);
+  };
+  return $tx->result;
+};
+
+
+# Catch koral errors
+sub _catch_koral_errors {
+  my $res = shift;
+
+  my $json = $res->json;
+
+  unless ($json) {
+    return Mojo::Promise->new->reject([
+      [undef, 'JSON response is invalid']
+    ]);
+  };
+
+  # Get errors
+  my $err = $json->{errors};
+
+  # Create error message
+  if ($err) {
+    return Mojo::Promise->new->reject($err);
+  };
+
+  if ($json->{status}) {
+    return Mojo::Promise->new->reject([
+      [undef, 'Middleware error ' . $json->{'status'}]
+    ]);
+  };
+
+  return $json;
+};
+
+
+# Notify the user in case of warnings
+sub _notify_on_warnings {
+  my ($self, $warnings) = @_;
+
+  # TODO: Check for ref!
+  foreach my $w (@$warnings) {
+    $self->notify(
+      warn =>
+        ($w->[0] ? $w->[0] . ': ' : '') .
+        $w->[1]
+      );
+  };
+};
+
+sub _notify_on_errors {
+  my ($self, $errors) = @_;
+  foreach my $e (@$errors) {
+    $self->notify(
+      error =>
+        ($e->[0] ? $e->[0] . ': ' : '') .
+        ($e->[1] || 'Unknown')
+      );
+  };
+};
+
+
+# Notify the user in case of errors
+#sub _notify_on_warnings {
+#  my ($self, $json) = @_;
+#
+#  if ($json->{warnings}) {
+#
+#    # TODO: Check for ref!
+#    foreach (@{$json->{warnings}}) {
+#      $self->notify(
+#        warn =>
+#          ($_->[0] ? $_->[0] . ': ' : '') .
+#          $_->[1]
+#        );
+#    };
+#  };
+#}
+
+
+# Process response and set stash values
+sub _process_matches {
+  my ($self, $json) = @_;
+
+    # Process meta
+  my $meta = $json->{meta};
+
+  # TODO:
+  #   Set benchmark in case of development mode only.
+  #   Use server timing API
+  #
+  # Reformat benchmark counter
+  # my $benchmark = $meta->{benchmark};
+  # if ($benchmark && $benchmark =~ s/\s+(m)?s$//) {
+  #   $benchmark = sprintf("%.2f", $benchmark) . ($1 ? $1 : '') . 's';
+  # };
+  # Set benchmark
+  # $index->benchmark($benchmark);
+
+  # Set time exceeded
+  if ($meta->{timeExceeded} && $meta->{timeExceeded} eq Mojo::JSON::true) {
+    $self->stash(time_exceeded => 1);
+  };
+
+  # Set result values
+  $self->stash(items_per_page => $meta->{itemsPerPage});
+
+  # Set authorization
+  # $index->authorized($meta->{authorized}) if $meta->{authorized};
+
+  # Bouncing query
+  #  if ($json->{query}) {
+  #    $index->query_jsonld($json->{query});
+  #  };
+
+  # Legacy
+  # elsif ($json->{request}->{query}) {
+  #   $index->query_jsonld($json->{request}->{query});
+  # };
+
+
+  if ($meta->{totalResults}) {
+    $self->stash(total_results => $meta->{totalResults});
+  };
+
+  # Bouncing collection query
+  if ($json->{corpus} || $json->{collection}) {
+    $self->stash(corpus_jsonld => ($json->{corpus} || $json->{collection}));
+  };
+
+  # Set results to stash
+  $self->stash(
+    results => _map_matches($json->{matches})
+  );
+
+  return;
+};
+
+
+# Cleanup array of matches
+sub _map_matches {
+  return c() unless $_[0];
+  c(map { _map_match($_) } @{ shift() });
+};
+
+
+# Cleanup single match
+sub _map_match {
+  my $match = shift or return;
+
+  # Legacy match id
+  if ($match->{matchID}) {
+    $match->{matchID} =~ s/^match\-(?:[^!]+!|[^_]+_)[^\.]+?\.[^-]+?-// or
+      $match->{matchID} =~ s!^match\-(?:[^\/]+\/){2}[^-]+?-!!;
+  };
+
+  # Set IDs based on the sigle
+  (
+    $match->{corpusID},
+    $match->{docID},
+    $match->{textID}
+  ) = ($match->{textSigle} =~ /^([^_]+?)_+([^\.]+?)\.(.+?)$/);
+
+  return $match;
+};
+
+
+# Query endpoint
 sub query {
   my $c = shift;
 
@@ -48,11 +228,12 @@
 
   # Create remote request URL
   my $url = Mojo::URL->new($c->api);
+  $url->path('search');
   $url->query(\%query);
 
   # Check if total results is cached
   my $total_results;
-  if (!$c->no_cache) {
+  if (!$c->no_cache && 0) {
     $total_results = $c->chi->get('total-' . $user . '-' . $url->to_string);
     $c->stash(total_results => $total_results);
     $c->app->log->debug('Get total result from cache');
@@ -61,8 +242,6 @@
     $url->query({cutoff => 'true'});
   };
 
-
-
   # Establish 'search_results' taghelper
   # This is based on Mojolicious::Plugin::Search
   $c->app->helper(
@@ -76,34 +255,121 @@
         return '';
       };
 
+      my $coll = $c->stash('results');
+
       # Iterate over results
-      my $string = $c->stash('search.results')->map(
+      my $string = $coll->map(
         sub {
           # Call hit callback
-          $c->stash('search.hit' => $_);
+          # $c->stash('search.hit' => $_);
           local $_ = $_[0];
           return $cb->($_);
         })->join;
 
       # Remove hit from stash
-      delete $c->stash->{'search.hit'};
+      # delete $c->stash->{'search.hit'};
       return b($string);
     }
   );
 
+  # Check if the request is cached
+  my $url_string = $url->to_string;
 
-  return $c->render(
-    template => 'search2',
-    q => $query,
-    ql => scalar $v->param('ql') // 'poliqarp',
-    result_size => 0,
-    start_page => 1,
-    total_pages => 20,
-    total_results => 40,
-    time_exceeded => 0,
-    benchmark => 'Long ...',
-    api_response => ''
-  );
+  # Set api request for debugging
+  # $c->stash('api_request' => $url_string);
+
+  # Debugging
+  $c->app->log->debug('Search for ' . $url_string);
+
+  # Check for cache
+  my $json = $c->chi->get('matches-' . $user . '-' . $url_string);
+
+  # Initialize promise object
+  my $promise;
+
+  # Result is cached
+  if ($json) {
+    $json->{cached} = 'true';
+
+    # The promise is already satisfied by the cache
+    $promise = Mojo::Promise->new->resolve($json);
+  }
+
+  # Retrieve from URL
+  else {
+
+    # Wrap a user agent method with a promise
+    $promise = $c->user->auth_request_p(get => $url)
+      ->then(\&_catch_http_errors)
+      ->then(\&_catch_koral_errors)
+      ;
+  };
+
+  $c->render_later;
+
+  # Prepare warnings
+  $promise->then(
+    sub {
+      my $json = shift;
+      $c->_notify_on_warnings($json->{warnings}) if $json->{warnings};
+      return $json
+    }
+
+  # Process response
+  )->then(
+    sub {
+      my $json = shift;
+
+      # Cache total results
+      unless ($c->stash('total_results') && $json->{meta}->{totalResults}) {
+
+        # Remove cutoff requirement again
+        $url->query([cutoff => 'true']);
+
+        # Set cache
+        $c->chi->set(
+          'total-' . $user . '-' . $url->to_string => $json->{meta}->{totalResults}
+        )
+      };
+
+      # Cache result
+      $c->chi->set('matches-' . $user . '-' . $url_string => $json);
+
+      # Process match results
+      return $c->_process_matches($json);
+    }
+
+  # Deal with errors
+  )->catch(
+    sub {
+      $c->_notify_on_errors(shift);
+    }
+
+  # Render template
+  )->finally(
+    sub {
+      # Choose the snippet based on the parameter
+      my $template = scalar $v->param('snippet') ? 'snippet' : 'search2';
+
+      $c->app->log->debug('Render template ...');
+
+      return $c->render(
+        template => $template,
+        q => $query,
+        ql => scalar $v->param('ql') // 'poliqarp',
+        results => c(),
+        start_page => 1,
+        total_pages => 20,
+        # total_results => 40,
+        time_exceeded => 0,
+        benchmark => 'Long ...',
+        api_response => ''
+      );
+    }
+  )->wait;
+
+
+  return 1;
 };
 
 
diff --git a/lib/Kalamar/Plugin/KalamarUser.pm b/lib/Kalamar/Plugin/KalamarUser.pm
index 211c87d..2f1a7a3 100644
--- a/lib/Kalamar/Plugin/KalamarUser.pm
+++ b/lib/Kalamar/Plugin/KalamarUser.pm
@@ -1,5 +1,6 @@
 package Kalamar::Plugin::KalamarUser;
 use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::Promise;
 use Mojo::ByteStream 'b';
 
 has 'api';
@@ -114,6 +115,33 @@
     }
   );
 
+  # Request with authorization header
+  # return a promise
+  $mojo->helper(
+    'user.auth_request_p' => sub {
+      my $c = shift;
+      my $method = shift;
+      my $path = shift;
+
+      my $ua = $plugin->ua;
+
+      my $tx;
+      if ($c->user_auth) {
+        $tx = $plugin->build_authorized_tx(
+          $c->user_auth, $c->client_ip, uc($method), $path, @_
+        );
+      }
+      else {
+        $tx = $ua->build_tx(
+          uc($method), $path, @_
+        );
+      };
+
+      # Create a promise object
+      return $ua->start_p($tx);
+    }
+  );
+
 
   # Login
   $mojo->helper(
diff --git a/t/fixture.t b/t/fixture.t
index 3fd0bd3..6fac855 100644
--- a/t/fixture.t
+++ b/t/fixture.t
@@ -32,6 +32,14 @@
   ->json_is('/errors/1/1','Could not parse query >>> [orth=das <<<.')
   ;
 
+$t->get_ok('/search?q=baum&ql=poliqarp')
+  ->status_is(200)
+  ->json_is('/meta/count', 25)
+  ->json_is('/meta/serialQuery', "tokens:s:Baum")
+  ->json_is('/matches/0/docSigle', "GOE/AGI")
+  ;
+
+
 done_testing;
 __END__
 
diff --git a/t/fixtures/fake_backend.pl b/t/fixtures/fake_backend.pl
index 73b4561..e7fc518 100644
--- a/t/fixtures/fake_backend.pl
+++ b/t/fixtures/fake_backend.pl
@@ -54,6 +54,7 @@
   shift->render(text => 'Fake server available');
 };
 
+
 # Search fixtures
 get '/search' => sub {
   my $c = shift;
@@ -64,6 +65,8 @@
   $v->optional('count');
   $v->optional('context');
 
+  $c->app->log->debug('Receive request');
+
   # Response q=x&ql=cosmas3
   if ($v->param('ql') && $v->param('ql') eq 'cosmas3') {
     return $c->render(
@@ -93,9 +96,12 @@
     $response->{json}->{meta}->{startIndex} = $v->param("startIndex");
   };
 
-
   # Simple search fixture
-  return $c->render(%$response);
+  $c->render(%$response);
+
+  $c->app->log->debug('Rendered result');
+
+  return 1;
 };
 
 
diff --git a/t/query.t b/t/query.t
index de05a78..75ed9b8 100644
--- a/t/query.t
+++ b/t/query.t
@@ -1,16 +1,37 @@
 use Mojo::Base -strict;
 use Test::Mojo;
 use Test::More;
+use Mojo::File qw/path/;
+
+
+#####################
+# Start Fake server #
+#####################
+my $mount_point = '/api/';
+$ENV{KALAMAR_API} = $mount_point;
 
 my $t = Test::Mojo->new('Kalamar');
 
+# Mount fake backend
+# Get the fixture path
+my $fixtures_path = path(Mojo::File->new(__FILE__)->dirname, 'fixtures');
+my $fake_backend = $t->app->plugin(
+  Mount => {
+    $mount_point =>
+      $fixtures_path->child('fake_backend.pl')
+  }
+);
+# Configure fake backend
+$fake_backend->pattern->defaults->{app}->log($t->app->log);
+
 # Query passed
-$t->get_ok('/q2?q=hui')
+$t->get_ok('/q2?q=baum')
   ->status_is(200)
   ->text_is('#error','')
-  ->text_is('title', 'KorAP: Find »hui« with Poliqarp')
-  ->element_exists('meta[name="DC.title"][content="KorAP: Find »hui« with Poliqarp"]')
+  ->text_is('title', 'KorAP: Find »baum« with Poliqarp')
+  ->element_exists('meta[name="DC.title"][content="KorAP: Find »baum« with Poliqarp"]')
   ->element_exists('body[itemscope][itemtype="http://schema.org/SearchResultsPage"]')
+  ->text_is('#total-results', 51)
   ;
 
 
diff --git a/t/remote_user.t b/t/remote_user.t
index 96897e5..ee0b872 100644
--- a/t/remote_user.t
+++ b/t/remote_user.t
@@ -4,6 +4,10 @@
 use Mojo::File qw/path/;
 use Data::Dumper;
 
+
+#####################
+# Start Fake server #
+#####################
 my $mount_point = '/api/';
 $ENV{KALAMAR_API} = $mount_point;
 
@@ -19,10 +23,10 @@
       $fixtures_path->child('fake_backend.pl')
   }
 );
-
 # Configure fake backend
 $fake_backend->pattern->defaults->{app}->log($t->app->log);
 
+
 $t->get_ok('/api')
   ->status_is(200)
   ->content_is('Fake server available');
@@ -58,6 +62,7 @@
   ->tx->res->dom->at('input[name=csrf_token]')->attr('value')
   ;
 
+
 $t->post_ok('/user/login' => form => { handle_or_email => 'test', pwd => 'pass', csrf_token => $csrf })
   ->status_is(302)
   ->header_is('Location' => '/');
diff --git a/templates/search2.html.ep b/templates/search2.html.ep
index 6ed9c9e..200d693 100644
--- a/templates/search2.html.ep
+++ b/templates/search2.html.ep
@@ -1,6 +1,6 @@
 % layout 'main', title => loc('searchtitle', q => stash('q'), ql => stash('ql')), schematype => 'SearchResultsPage';
 
-<div id="resultinfo" <% if (stash('results_size')) { %> class="found"<%} %>>
+<div id="resultinfo" <% if (stash('results')->size) { %> class="found"<%} %>>
   <div id="pagination"><%= pagination(stash('start_page'), stash('total_pages'), url_with->query(['p' => '{page}'])) =%></div>
 % my $found = stash('total_results');
   <p class="found">\
@@ -11,7 +11,7 @@
 %   };
 <span id="total-results"><%= $found_text %></span> <%= loc('matchCount', found => $found) %>\
 %# <% if (stash('benchmark')) { %> (~ <%= stash('benchmark') %>)<% } %>
-% } elsif (stash('start_index') == 0 && stash('result_size') == 0) {
+% } elsif (stash('start_index') == 0 && stash('results')->size == 0) {
 <span id="total-results">0</span> <%= loc('matchCount', found => $found) %>\
 % };
 </p>
@@ -20,13 +20,13 @@
 %= include 'query2'
 
 <div id="search">
-% if (stash('total_results') != 0 && stash('result_size')) {
+% if (stash('total_results') != 0 && stash('results')->size) {
   <ol class="align-left">
 %=  search_results begin
 %=    include 'match', match => $_
 %   end
   </ol>
-% } elsif (stash('result_size') == 0) {
+% } elsif (stash('results')->size == 0) {
 <p id="no-results"><%= loc 'noMatches', q => stash('q'), ql => stash('ql') %></p>
 % }
 </div>