From c83cd774117f808aab27ece79fc31c9c44d4fffc Mon Sep 17 00:00:00 2001 From: Julian Maurice Date: Wed, 4 Mar 2015 16:46:33 +0100 Subject: [PATCH] Bug 13799: RESTful API with Mojolicious and Swagger2 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 Signed-off-by: Tomas Cohen Arazi Signed-off-by: Martin Renvoize Signed-off-by: Kyle M Hall Signed-off-by: Tomas Cohen Arazi --- C4/Installer/PerlDependencies.pm | 10 +++ Koha/Object.pm | 12 ++++ Koha/Objects.pm | 12 ++++ Koha/REST/V1.pm | 28 ++++++++ Koha/REST/V1/Borrowers.pm | 29 ++++++++ api/v1/app.pl | 6 ++ api/v1/swagger.json | 108 ++++++++++++++++++++++++++++++ etc/koha-httpd.conf | 56 ++++++++++++++++ t/db_dependent/api/v1/borrowers.t | 37 ++++++++++ 9 files changed, 298 insertions(+) create mode 100644 Koha/REST/V1.pm create mode 100644 Koha/REST/V1/Borrowers.pm create mode 100755 api/v1/app.pl create mode 100644 api/v1/swagger.json create mode 100644 t/db_dependent/api/v1/borrowers.t diff --git a/C4/Installer/PerlDependencies.pm b/C4/Installer/PerlDependencies.pm index 0e5057b96f..765151e901 100644 --- a/C4/Installer/PerlDependencies.pm +++ b/C4/Installer/PerlDependencies.pm @@ -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; diff --git a/Koha/Object.pm b/Koha/Object.pm index 93fdbe0dfe..34776a124c 100644 --- a/Koha/Object.pm +++ b/Koha/Object.pm @@ -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 diff --git a/Koha/Objects.pm b/Koha/Objects.pm index 8ef6390f7a..1ff91cb468 100644 --- a/Koha/Objects.pm +++ b/Koha/Objects.pm @@ -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 index 0000000000..39cdab90d1 --- /dev/null +++ b/Koha/REST/V1.pm @@ -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 index 0000000000..b58ad2a978 --- /dev/null +++ b/Koha/REST/V1/Borrowers.pm @@ -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 index 0000000000..55ce87d0cb --- /dev/null +++ b/api/v1/app.pl @@ -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 index 0000000000..9672f153fe --- /dev/null +++ b/api/v1/swagger.json @@ -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" + } + } +} diff --git a/etc/koha-httpd.conf b/etc/koha-httpd.conf index f9a1970767..4f1fc470d7 100644 --- a/etc/koha-httpd.conf +++ b/etc/koha-httpd.conf @@ -112,6 +112,20 @@ 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" + + 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] + @@ -214,5 +228,47 @@ 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" + + 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] + + +# Uncomment this VirtualHost to enable API access through +# api.__WEBSERVER_HOST__:__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 +# +# +# +# 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] +# +# +# diff --git a/t/db_dependent/api/v1/borrowers.t b/t/db_dependent/api/v1/borrowers.t new file mode 100644 index 0000000000..43e5af0892 --- /dev/null +++ b/t/db_dependent/api/v1/borrowers.t @@ -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; -- 2.39.5