Bug 13799: RESTful API with Mojolicious and Swagger2
authorJulian Maurice <julian.maurice@biblibre.com>
Wed, 4 Mar 2015 15:46:33 +0000 (16:46 +0100)
committerTomas Cohen Arazi <tomascohen@theke.io>
Wed, 4 Nov 2015 16:47:32 +0000 (13:47 -0300)
Actual routes are:
  /borrowers
    Return a list of all borrowers in Koha

  /borrowers/{borrowernumber}
    Return the borrower identified by {borrowernumber}
    (eg. /borrowers/1)

There is a test file you can run with:
  $ prove t/db_dependent/rest/borrowers.t

All API stuff is in /api/v1 (except Perl modules)
So we have:
  /api/v1/script.cgi     CGI script
  /api/v1/swagger.json   Swagger specification

Change both OPAC and Intranet VirtualHosts to access the API,
so we have:
  http://OPAC/api/v1/swagger.json   Swagger specification
  http://OPAC/api/v1/{path}         API endpoint
  http://INTRANET/api/v1/swagger.json   Swagger specification
  http://INTRANET/api/v1/{path}         API endpoint

Add a (disabled) virtual host in Apache configuration api.HOSTNAME,
so we have:
  http://api.HOSTNAME/api/v1/swagger.json   Swagger specification
  http://api.HOSTNAME/api/v1/{path}         API endpoint

Add 'unblessed' subroutines to both Koha::Objects and Koha::Object to be
able to pass it to Mojolicious

Test plan:
  1/ Install Perl modules Mojolicious and Swagger2
  2/ perl Makefile.PL
  3/ make && make install
  4/ Change etc/koha-httpd.conf and copy it to the right place if needed
  5/ Reload Apache
  6/ Check that http://(OPAC|INTRANET)/api/v1/borrowers and
     http://(OPAC|INTRANET)/api/v1/borrowers/{borrowernumber} works

Optionally, you could verify that http://(OPAC|INTRANET)/vX/borrowers
(where X is an integer greater than 1) returns a 404 error

Signed-off-by: Alex Arnaud <alex.arnaud@biblibre.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
C4/Installer/PerlDependencies.pm
Koha/Object.pm
Koha/Objects.pm
Koha/REST/V1.pm [new file with mode: 0644]
Koha/REST/V1/Borrowers.pm [new file with mode: 0644]
api/v1/app.pl [new file with mode: 0755]
api/v1/swagger.json [new file with mode: 0644]
etc/koha-httpd.conf
t/db_dependent/api/v1/borrowers.t [new file with mode: 0644]

index 0e5057b..765151e 100644 (file)
@@ -762,6 +762,16 @@ our $PERL_DEPS = {
         'required' => '1',
         'min_ver'  => '0.05',
     },
+    'Mojolicious' => {
+        'usage'    => 'REST API',
+        'required' => '0',
+        'min_ver'  => '5.54',
+    },
+    'Swagger2' => {
+        'usage'    => 'REST API',
+        'required' => '0',
+        'min_ver'  => '0.28',
+    },
 };
 
 1;
index 93fdbe0..34776a1 100644 (file)
@@ -207,6 +207,18 @@ sub id {
     return $id;
 }
 
+=head3 $object->unblessed();
+
+Returns an unblessed representation of object.
+
+=cut
+
+sub unblessed {
+    my ($self) = @_;
+
+    return { $self->_result->get_columns };
+}
+
 =head3 $object->_result();
 
 Returns the internal DBIC Row object
index 8ef6390..1ff91cb 100644 (file)
@@ -181,6 +181,18 @@ sub as_list {
     return wantarray ? @objects : \@objects;
 }
 
+=head3 Koha::Objects->unblessed
+
+Returns an unblessed representation of objects.
+
+=cut
+
+sub unblessed {
+    my ($self) = @_;
+
+    return [ map { $_->unblessed } $self->as_list ];
+}
+
 =head3 Koha::Objects->_wrap
 
 wraps the DBIC object in a corresponding Koha object
diff --git a/Koha/REST/V1.pm b/Koha/REST/V1.pm
new file mode 100644 (file)
index 0000000..39cdab9
--- /dev/null
@@ -0,0 +1,28 @@
+package Koha::REST::V1;
+
+use Modern::Perl;
+use Mojo::Base 'Mojolicious';
+
+sub startup {
+    my $self = shift;
+
+    my $route = $self->routes->under->to(
+        cb => sub {
+            my $c = shift;
+            my $user = $c->param('user');
+            # Do the authentication stuff here...
+            $c->stash('user', $user);
+            return 1;
+        }
+    );
+
+    # Force charset=utf8 in Content-Type header for JSON responses
+    $self->types->type(json => 'application/json; charset=utf8');
+
+    $self->plugin(Swagger2 => {
+        route => $route,
+        url => $self->home->rel_file("api/v1/swagger.json"),
+    });
+}
+
+1;
diff --git a/Koha/REST/V1/Borrowers.pm b/Koha/REST/V1/Borrowers.pm
new file mode 100644 (file)
index 0000000..b58ad2a
--- /dev/null
@@ -0,0 +1,29 @@
+package Koha::REST::V1::Borrowers;
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Controller';
+
+use Koha::Borrowers;
+
+sub list_borrowers {
+    my ($c, $args, $cb) = @_;
+
+    my $borrowers = Koha::Borrowers->search;
+
+    $c->$cb($borrowers->unblessed, 200);
+}
+
+sub get_borrower {
+    my ($c, $args, $cb) = @_;
+
+    my $borrower = Koha::Borrowers->find($args->{borrowernumber});
+
+    if ($borrower) {
+        return $c->$cb($borrower->unblessed, 200);
+    }
+
+    $c->$cb({error => "Borrower not found"}, 404);
+}
+
+1;
diff --git a/api/v1/app.pl b/api/v1/app.pl
new file mode 100755 (executable)
index 0000000..55ce87d
--- /dev/null
@@ -0,0 +1,6 @@
+#!/usr/bin/env perl
+
+use Modern::Perl;
+
+require Mojolicious::Commands;
+Mojolicious::Commands->start_app('Koha::REST::V1');
diff --git a/api/v1/swagger.json b/api/v1/swagger.json
new file mode 100644 (file)
index 0000000..9672f15
--- /dev/null
@@ -0,0 +1,108 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "title": "Koha REST API",
+    "version": "1",
+    "license": {
+      "name": "GPL v3",
+      "url": "http://www.gnu.org/licenses/gpl.txt"
+    },
+    "contact": {
+      "name": "Koha Team",
+      "url": "http://koha-community.org/"
+    }
+  },
+  "basePath": "/api/v1",
+  "paths": {
+    "/borrowers": {
+      "get": {
+        "x-mojo-controller": "Koha::REST::V1::Borrowers",
+        "operationId": "listBorrowers",
+        "tags": ["borrowers"],
+        "produces": [
+          "application/json"
+        ],
+        "responses": {
+          "200": {
+            "description": "A list of borrowers",
+            "schema": {
+              "type": "array",
+              "items": {
+                "$ref": "#/definitions/borrower"
+              }
+            }
+          }
+        }
+      }
+    },
+    "/borrowers/{borrowernumber}": {
+      "get": {
+        "x-mojo-controller": "Koha::REST::V1::Borrowers",
+        "operationId": "getBorrower",
+        "tags": ["borrowers"],
+        "parameters": [
+          {
+            "$ref": "#/parameters/borrowernumberPathParam"
+          }
+        ],
+        "produces": [
+          "application/json"
+        ],
+        "responses": {
+          "200": {
+            "description": "A borrower",
+            "schema": {
+              "$ref": "#/definitions/borrower"
+            }
+          },
+          "404": {
+            "description": "Borrower not found",
+            "schema": {
+              "$ref": "#/definitions/error"
+            }
+          }
+        }
+      }
+    }
+  },
+  "definitions": {
+    "borrower": {
+      "type": "object",
+      "properties": {
+        "borrowernumber": {
+          "$ref": "#/definitions/borrowernumber"
+        },
+        "cardnumber": {
+          "description": "library assigned ID number for borrowers"
+        },
+        "surname": {
+          "description": "borrower's last name"
+        },
+        "firstname": {
+          "description": "borrower's first name"
+        }
+      }
+    },
+    "borrowernumber": {
+      "description": "Borrower internal identifier"
+    },
+    "error": {
+      "type": "object",
+      "properties": {
+        "error": {
+          "description": "Error message",
+          "type": "string"
+        }
+      }
+    }
+  },
+  "parameters": {
+    "borrowernumberPathParam": {
+      "name": "borrowernumber",
+      "in": "path",
+      "description": "Internal borrower identifier",
+      "required": "true",
+      "type": "integer"
+    }
+  }
+}
index f9a1970..4f1fc47 100644 (file)
      RewriteRule ^/bib/([^\/]*)/?$ /cgi-bin/koha/opac-detail\.pl?bib=$1 [PT]
      RewriteRule ^/isbn/([^\/]*)/?$ /search?q=isbn:$1 [PT]
      RewriteRule ^/issn/([^\/]*)/?$ /search?q=issn:$1 [PT]
+
+     # REST API configuration
+     Alias "/api" "__OPAC_CGI_DIR__/api"
+     <Directory __OPAC_CGI_DIR__/api>
+       Options +ExecCGI +FollowSymlinks
+       AddHandler cgi-script .pl
+
+       RewriteEngine On
+       RewriteBase /api/
+       RewriteCond %{REQUEST_FILENAME} !-f
+       RewriteCond %{REQUEST_FILENAME} !-d
+       RewriteCond %{DOCUMENT_ROOT}/../api/$1/app.pl -f
+       RewriteRule ^(.*?)/.* $1/app.pl/api/$0 [L]
+     </Directory>
    </IfModule>
 </VirtualHost>
 
      RewriteRule ^/bib/([^\/]*)/?$ /cgi-bin/koha/detail\.pl?bib=$1 [PT]
      RewriteRule ^/isbn/([^\/]*)/?$ /search?q=isbn:$1 [PT]
      RewriteRule ^/issn/([^\/]*)/?$ /search?q=issn:$1 [PT]
+
+
+     # REST API configuration
+     Alias "/api" "__INTRANET_CGI_DIR__/api"
+     <Directory __INTRANET_CGI_DIR__/api>
+       Options +ExecCGI +FollowSymlinks
+       AddHandler cgi-script .pl
+
+       RewriteEngine On
+       RewriteBase /api/
+       RewriteCond %{REQUEST_FILENAME} !-f
+       RewriteCond %{REQUEST_FILENAME} !-d
+       RewriteCond %{DOCUMENT_ROOT}/../api/$1/app.pl -f
+       RewriteRule ^(.*?)/.* $1/app.pl/api/$0 [L]
+     </Directory>
    </IfModule>
 </VirtualHost>
+
+# Uncomment this VirtualHost to enable API access through
+# api.__WEBSERVER_HOST__:__WEBSERVER_PORT__
+#<VirtualHost __WEBSERVER_IP__:__WEBSERVER_PORT__>
+#  ServerAdmin __WEBMASTER_EMAIL__
+#  DocumentRoot __INTRANET_CGI_DIR__/api
+#  ServerName api.__WEBSERVER_HOST__:__WEBSERVER_PORT__
+#  SetEnv KOHA_CONF "__KOHA_CONF_DIR__/koha-conf.xml"
+#  SetEnv PERL5LIB "__PERL_MODULE_DIR__"
+#  ErrorLog __LOG_DIR__/koha-api-error_log
+#
+#  <IfModule mod_rewrite.c>
+#    <Directory __INTRANET_CGI_DIR__/api>
+#      Options +ExecCGI +FollowSymlinks
+#      AddHandler cgi-script .pl
+#
+#      RewriteEngine on
+#
+#      RewriteRule ^api/(.*) $1 [L]
+#
+#      RewriteCond %{REQUEST_FILENAME} !-f
+#      RewriteCond %{REQUEST_FILENAME} !-d
+#      RewriteCond %{DOCUMENT_ROOT}/$1/app.pl -f
+#      RewriteRule ^(.*?)/.* $1/app.pl/api/$0 [L]
+#    </Directory>
+#  </IfModule>
+#</VirtualHost>
diff --git a/t/db_dependent/api/v1/borrowers.t b/t/db_dependent/api/v1/borrowers.t
new file mode 100644 (file)
index 0000000..43e5af0
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/env perl
+
+use Modern::Perl;
+
+use Test::More tests => 6;
+use Test::Mojo;
+
+use C4::Context;
+
+use Koha::Database;
+use Koha::Borrower;
+
+my $dbh = C4::Context->dbh;
+$dbh->{AutoCommit} = 0;
+$dbh->{RaiseError} = 1;
+
+my $t = Test::Mojo->new('Koha::REST::V1');
+
+my $categorycode = Koha::Database->new()->schema()->resultset('Category')->first()->categorycode();
+my $branchcode = Koha::Database->new()->schema()->resultset('Branch')->first()->branchcode();
+
+my $borrower = Koha::Borrower->new;
+$borrower->categorycode( $categorycode );
+$borrower->branchcode( $branchcode );
+$borrower->surname("Test Surname");
+$borrower->store;
+my $borrowernumber = $borrower->borrowernumber;
+
+$t->get_ok('/api/v1/borrowers')
+  ->status_is(200);
+
+$t->get_ok("/api/v1/borrowers/$borrowernumber")
+  ->status_is(200)
+  ->json_is('/borrowernumber' => $borrowernumber)
+  ->json_is('/surname' => "Test Surname");
+
+$dbh->rollback;