This patch adds plugins the capability of injecting new routes on the
API.
The plugins should provide the following methods to be considered valid API-generating plugins:
- 'api_routes': returning the 'path' component of the OpenAPI specification corresponding to the routes served by the plugin
- 'api_namespace': it should return a namespace to be used for grouping the endpoints provided by the plugin
otherwise, they will be just skipped.
All plugin-generated routes will be added the 'contrib' namespace, and
will end up placed inside /contrib/<namespace>, where <namespace> is what the 'api_namespace' returns.
A sample endpoint will be added to the Kitchen Sink plugin, and tests
are being written.
To test:
- Apply this patches
- Run:
$ kshell
k$ prove t/db_dependent/Koha/REST/Plugin/PluginRoutes.t
=> SUCCESS: Tests pass!
- Install the (latest) KitchenSink plugin
- Point your browser to the API like this:
http://koha-intra.myDNSname.org:8081/api/v1/.html
=> SUCCESS: The /contrib/kitchensink/patrons/:patron_id/bother endpoint
implemented by the plugin has been merged!
- Sign off! :-D
Signed-off-by: Benjamin Rokseth <benjamin.rokseth@deichman.no>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
Signed-off-by: Alex Arnaud <alex.arnaud@biblibre.com>
Signed-off-by: Nick Clemens <nick@bywatersolutions.com>
(cherry picked from commit
3fedae85f25ef5f587d567b51b86aab776d87311)
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
(cherry picked from commit
ab79f3fc67a06ea7383270ae7f00f97605c3a4d0)
Signed-off-by: Fridolin Somers <fridolin.somers@biblibre.com>
--- /dev/null
+package Koha::Exceptions::Plugin;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Koha::Exceptions::Exception;
+
+use Exception::Class (
+ 'Koha::Exceptions::Plugin' => {
+ isa => 'Koha::Exceptions::Exception',
+ },
+ 'Koha::Exceptions::Plugin::ForbiddenAction' => {
+ isa => 'Koha::Exceptions::Plugin',
+ description => 'The plugin is trying to do something it is not allowed to'
+ },
+ 'Koha::Exceptions::Plugin::MissingMethod' => {
+ isa => 'Koha::Exceptions::Plugin',
+ description => 'Required method is missing',
+ fields => ['plugin_name','method']
+ }
+);
+
+sub full_message {
+ my $self = shift;
+
+ my $msg = $self->message;
+
+ unless ( $msg) {
+ if ( $self->isa('Koha::Exceptions::Plugin::MissingMethod') ) {
+ $msg = sprintf("Cannot use plugin (%s) because the it doesn't implement the '%s' method which is required.", $self->plugin_name, $self->method );
+ }
+ }
+
+ return $msg;
+}
+
+=head1 NAME
+
+Koha::Exceptions::Plugin - Base class for Plugin exceptions
+
+=head1 Exceptions
+
+=head2 Koha::Exceptions::Plugin
+
+Generic Plugin exception
+
+=head2 Koha::Exceptions::Plugin::MissingMethod
+
+Exception to be used when a plugin is required to implement a specific
+method and it doesn't.
+
+=head3 Parameters
+
+=over
+
+=item plugin_name: the plugin name for display purposes
+
+=item method: the missing method
+
+=back
+
+=head1 Class methods
+
+=head2 full_message
+
+Overloaded method for exception stringifying.
+
+=cut
+
+1;
--- /dev/null
+package Koha::REST::Plugin::PluginRoutes;
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with Koha; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use Modern::Perl;
+
+use Mojo::Base 'Mojolicious::Plugin';
+
+use Koha::Exceptions::Plugin;
+use Koha::Plugins;
+
+use Clone qw(clone);
+use Try::Tiny;
+
+=head1 NAME
+
+Koha::REST::Plugin::PluginRoutes
+
+=head1 API
+
+=head2 Helper methods
+
+=head3 register
+
+=cut
+
+sub register {
+ my ( $self, $app, $config ) = @_;
+
+ my $spec = $config->{spec};
+ my $validator = $config->{validator};
+
+ my @plugins;
+
+ if ( C4::Context->preference('UseKohaPlugins')
+ && C4::Context->config("enable_plugins") )
+ {
+ @plugins = Koha::Plugins->new()->GetPlugins(
+ {
+ method => 'api_routes',
+ }
+ );
+ # plugin needs to define a namespace
+ @plugins = grep { $_->api_namespace } @plugins;
+ }
+
+ foreach my $plugin ( @plugins ) {
+ $spec = inject_routes( $spec, $plugin, $validator );
+ }
+
+ return $spec;
+}
+
+=head3 inject_routes
+
+=cut
+
+sub inject_routes {
+ my ( $spec, $plugin, $validator ) = @_;
+
+ return try {
+
+ my $backup_spec = merge_spec( clone($spec), $plugin );
+ if ( spec_ok( $backup_spec, $validator ) ) {
+ $spec = merge_spec( $spec, $plugin );
+ }
+ else {
+ Koha::Exceptions::Plugin->throw(
+ "The resulting spec is invalid. Skipping " . $plugin->get_metadata->{name}
+ );
+ }
+
+ return $spec;
+ }
+ catch {
+ warn "$_";
+ return $spec;
+ };
+}
+
+=head3 merge_spec
+
+=cut
+
+sub merge_spec {
+ my ( $spec, $plugin ) = @_;
+
+ my $plugin_spec = $plugin->api_routes;
+
+ foreach my $route ( keys %{ $plugin_spec } ) {
+
+ my $THE_route = '/contrib/' . $plugin->api_namespace . $route;
+ if ( exists $spec->{ $THE_route } ) {
+ # Route exists, overwriting is forbidden
+ Koha::Exceptions::Plugin::ForbiddenAction->throw(
+ "Attempted to overwrite $THE_route"
+ );
+ }
+
+ $spec->{'paths'}->{ $THE_route } = $plugin_spec->{ $route };
+ }
+
+ return $spec;
+}
+
+=head3 spec_ok
+
+=cut
+
+sub spec_ok {
+ my ( $spec, $validator ) = @_;
+
+ return try {
+ $validator->load_and_validate_schema(
+ $spec,
+ {
+ allow_invalid_ref => 1,
+ }
+ );
+ return 1;
+ }
+ catch {
+ return 0;
+ }
+}
+
+1;
use Mojo::Base 'Mojolicious';
use C4::Context;
+use JSON::Validator::OpenAPI::Mojolicious;
=head1 NAME
$self->secrets([$secret_passphrase]);
}
- $self->plugin(OpenAPI => {
- url => $self->home->rel_file("api/v1/swagger/swagger.json"),
- route => $self->routes->under('/api/v1')->to('Auth#under'),
- allow_invalid_ref => 1, # required by our spec because $ref directly under
- # Paths-, Parameters-, Definitions- & Info-object
- # is not allowed by the OpenAPI specification.
- });
+ my $validator = JSON::Validator::OpenAPI::Mojolicious->new;
+ $validator->load_and_validate_schema(
+ $self->home->rel_file("api/v1/swagger/swagger.json"),
+ {
+ allow_invalid_ref => 1,
+ }
+ );
+
+ push @{$self->routes->namespaces}, 'Koha::Plugin';
+
+ my $spec = $validator->schema->data;
+ $self->plugin(
+ 'Koha::REST::Plugin::PluginRoutes' => {
+ spec => $spec,
+ validator => $validator
+ }
+ );
+
+ $self->plugin(
+ OpenAPI => {
+ spec => $spec,
+ route => $self->routes->under('/api/v1')->to('Auth#under'),
+ allow_invalid_ref =>
+ 1, # required by our spec because $ref directly under
+ # Paths-, Parameters-, Definitions- & Info-object
+ # is not allowed by the OpenAPI specification.
+ }
+ );
+
$self->plugin( 'Koha::REST::Plugin::Pagination' );
}