Skip to content

Commit

Permalink
Centralization of some OpenAPI docs (#7386)
Browse files Browse the repository at this point in the history
Centralization and expansion of (some) OpenAPI docs
  • Loading branch information
ehuelsmann committed Jun 1, 2023
1 parent 6f76558 commit fa6dd53
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 292 deletions.
51 changes: 34 additions & 17 deletions lib/LedgerSMB/Router.pm
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ use warnings;
use parent 'Exporter';

use Carp;
use Hash::Merge;
use HTML::Escape qw( escape_html );
use HTTP::Negotiate qw( choose );
use HTTP::Status qw( HTTP_BAD_REQUEST HTTP_NOT_FOUND HTTP_UNSUPPORTED_MEDIA_TYPE
Expand Down Expand Up @@ -80,6 +81,7 @@ use constant {

my $appname;
my $router = {};
my @cumulative_settings = qw/ api_schema /;


our @EXPORT = ## no critic (ProhibitAutomaticExportation)
Expand All @@ -106,7 +108,9 @@ sub import {

$appname = $args{appname} // caller;
$router->{$appname} //= __PACKAGE__->new;
$router->{$appname}->{settings} = {};
$router->{$appname}->{settings} = {
$router->{$appname}->{settings}->%{@cumulative_settings}
};

my @keywords = @{$args{keywords} // []};
__PACKAGE__->export_to_level(1, $pkg, @{$args{keywords}});
Expand All @@ -119,8 +123,6 @@ sub _alloc_entry {

=head2 new (constructor)
=cut

sub new {
Expand Down Expand Up @@ -196,6 +198,21 @@ sub add_mapping {
return;
}

=head2 api_validator()
Returns a C<JSONSchema::Validator> instance initialized from the accumulated
schema fragment definitions declared in the various router definition modules.
=cut

sub api_validator {
return $_[0]->{_validator} //=
JSONSchema::Validator->new(
schema => $_[0]->setting('api_schema'),
specification => 'OAS30');
}


=head2 lookup($path)
Matches C<$path> against the registered routes, finding the most specific
Expand Down Expand Up @@ -281,7 +298,6 @@ sub dispatch {
=head2 hooks($name [ => @hooks])
=cut

sub hooks {
Expand Down Expand Up @@ -451,13 +467,14 @@ sub put { _add_mapping(['put'], @_); }

sub api {
my ($path, $code) = @_;
my $schema = $router->{$appname}->setting('api_schema');
my $settings = $router->{$appname};

return (
$path => sub {
my @args = @_;
my $env = shift @args;
my $params = shift @args;
my $schema = $settings->api_validator;
my $req = Plack::Request::WithEncoding->new($env);
my $has_body = ($req->headers->content_length() // 0) > 0;
my $body = ($req->headers->content_type eq 'application/json') ?
Expand Down Expand Up @@ -521,19 +538,20 @@ sub api {
})
}

my $reader = YAML::PP->new(boolean => 'JSON::PP');
my $merger = Hash::Merge->new('LEFT_PRECEDENT');
sub openapi_schema {
my $fh = shift;
my $schema_text = do {
# slurp __DATA__ section
local $/ = undef;
<$fh>;
};

my $reader = YAML::PP->new(boolean => 'JSON::PP');
my $schema = $reader->load_string(
do {
# slurp __DATA__ section
local $/ = undef;
<$fh>;
});
return JSONSchema::Validator->new(
schema => $schema,
specification => 'OAS30');
my $rv = $merger->merge(
$router->{$appname}->setting('api_schema') // {},
$reader->load_string($schema_text));
return $rv;
}

my $variants = [
Expand Down Expand Up @@ -616,8 +634,7 @@ sub locale {
sub set {
my ($setting, $value) = @_;

$router->{$appname}->setting($setting, $value);

$router->{$appname}->setting($setting => $value);
return;
}

Expand Down
86 changes: 81 additions & 5 deletions lib/LedgerSMB/Routes/ERP/API.pm
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ This module doesn't export any methods.
use strict;
use warnings;

use LedgerSMB::Router appname => 'erp/api';


set api_schema => openapi_schema(\*DATA);


=head1 LICENSE AND COPYRIGHT
Expand All @@ -40,24 +42,98 @@ your software.


__DATA__
openapi: 3.0.0
openapi: 3.0.3
info:
title: LedgerSMB API
version: 0.0.1
contact:
name: "LedgerSMB API Support"
url: "https://github.com/ledgersmb/LedgerSMB/issues"
description: LedgerSMB API
email: devel@lists.ledgersmb.org
description: |
LedgerSMB comes with a web service API. The version number assigned follows
the [rules of semantic versioning](https://semver.org/). The current major
version is 0 (zero), meaning that it's not considered to have stabilized
yet. The main reason is that not all functions have been ironed out yet:
filtering, sorting and pagination are to be specified and implemented.
The API is hosted on `erp/api/v0`, on the same root as `login.pl`. That is
to say that if LedgerSMB's login screen is hosted at
`https://example.org/login.pl` then the API can be found at
`https://example.org/erp/api/v0`. All paths mentioned in this document will
be appended to that. E.g. the items in the menu for the authenticated user
can be accessed through `https://example.org/erp/api/v0/menu-nodes`.
license:
name: GPL-2.0-or-later
url: https://spdx.org/licenses/GPL-2.0-or-later.html
servers:
servers:
- url: 'http://lsmb/erp/api/v0'
security:
- cookieAuth: []
components:
headers:
ETag:
description: |
The API uses the ETag parameter to prevent different clients modifying
the same resource around the same time from overwriting each other's
data: the later updates will be rejected based on verification of this
parameter.
Clients need to retain the ETag returned on a request when they might
want to update the values later.
required: true
schema:
type: string
parameters:
if-match:
name: If-Match
in: header
description: |
Clients need to provide the If-Match parameter on update operations
(PUT and PATCH) with the ETag obtained in the request from which
data are being updated. Requests missing this header will be
rejected with HTTP response code 428. Requests trying to update
outdated content will be rejected with HTTP response code 412.
required: true
schema:
type: string
responses:
304:
description: Not modified
400:
description: Bad request
401:
description: Unauthorized
403:
description: Forbidden
404:
description: Not Found
412:
description: Precondition failed (If-Match header)
413:
description: Payload too large
428:
description: Precondition required
securitySchemes:
cookieAuth:
type: apiKey
in: cookie
name: LedgerSMB-1.10
security:
- cookieAuth: []
description: |
The authenticating cookie can be obtained by sending a `POST` request
to `login.pl?action=authenticate&company=<url-encoded-company` with a
JSON object in the body of the request, containing these three fields
* company
* username
* password
as if they had been entered on the login page in the browser. Please
note that the request `Content-Type` must be set to `application/json`
and that an `X-Requested-With` header is expected with the value
`XMLHttpRequest`.
**Note**: the cookie value is updated on each response; the next
request *must* be executed with the new cookie value.
**Note 2**: the validity of the cookie is as long the user's timeout
when logged into the application (default: 90 minutes).
Loading

0 comments on commit fa6dd53

Please sign in to comment.