Tutorial: Mojolicious::Plugin::OpenAPI: Parameters

Jonas Brømsø - Jul 27 '18 - - Dev Community

Tutorial on Mojolicious::Plugin::OpenAPI: Parameters

This is a follow up to my tutorial on Mojolicious::Plugin::OpenAPI, demonstrating the basic implementation of a Hello World example.

This tutorial with extend on the topic and will touch on parameter use.

The Application

The application implements and demonstrates two approaches to parameter handling:

  1. Query parameter based, like: /api/user?id=alice
  2. URL parameter based, like: /api/user/bob

First lets generate our application:

$ mojo generate app Parameters
Enter fullscreen mode Exit fullscreen mode

Jump into our newly generated application directory

$ cd parameters
Enter fullscreen mode Exit fullscreen mode

We then install the plugin we need to enable OpenAPI in our Mojolicious application

Using CPAN shell:

$ perl -MCPAN -e shell install Mojolicious::Plugin::OpenAPI
Enter fullscreen mode Exit fullscreen mode

Using cpanm:

$ cpanm Mojolicious::Plugin::OpenAPI
Enter fullscreen mode Exit fullscreen mode

If you need help installing please refer to the CPAN installation guide.

Create a definition JSON file based on OpenAPI to support an Hello World implementation based on the OpenAPI specification:

$ touch openapi.conf
Enter fullscreen mode Exit fullscreen mode

Open openapi.conf and insert the following snippet:

{
    "swagger": "2.0",
    "info": { "version": "1.0", "title": "Demo of API with parameters" },
    "basePath": "/api",
    "paths": {
      "/users": {
        "get": {
          "operationId": "getUsers",
          "x-mojo-name": "get_users",
          "x-mojo-to": "user#list",
          "summary": "Lists users",
          "responses": {
            "200": {
              "description": "Users response",
              "schema": {
                "type": "object",
                "properties": {
                  "users": {
                    "type": "array",
                    "items": { "type": "object" }
                  }
                }
              }
            }
          }
        }
      },
      "/user/#id": {
        "get": {
          "operationId": "getUserByUrl",
          "x-mojo-name": "get_user_by_url",
          "x-mojo-to": "user#get_by_url",
          "summary": "User response",
          "responses": {
            "200": {
              "description": "User response",
              "schema": {
                "type": "object",
                "properties": {
                  "user": {
                    "type": "object"
                  }
                }
              }
            },
            "default": {
              "description": "Unexpected error",
              "schema": {}
            }
          }
        }
      },
      "/user": {
            "get": {
              "operationId": "getUserByParameter",
              "x-mojo-name": "get_user_by_parameter",
              "x-mojo-to": "user#get_by_parameter",
              "summary": "User response",
              "parameters": [
                {"in": "query", "name": "id", "type": "string"}
              ],
              "responses": {
                "200": {
                  "description": "User response",
                  "schema": {
                    "type": "object",
                    "properties": {
                      "user": {
                        "type": "object"
                      }
                    }
                  }
                },
                "default": {
                  "description": "Unexpected error",
                  "schema": {}
                }
              }
            }
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Now lets go over our definiton. We will not cover the basics as such, please refer to the Hello World tutorial, instead we will focus on the aspects related to parameters.

We have introduced 3 end-points, to simulate a more complete RESTful API.

We have defined the following paths:

  • /api/users, returning an array of users. This is basically a variation of the Hello World example, which just returned an object where this end-point return an array of objects.

  • /api/user?id=«id», returning a user object if one can be found, this is based on a query parameter. Mojolicious::Plugin::OpenAPI validates this parameter based on the defintion in our openapi.json

  • /api/user/«id», returning a user object if one can be found, this is based on the URL like a proper RESTful interface, Mojolicious::Plugin::OpenAPI does NOT validate but the URL is somewhat validated based on the defintion in our openapi.json

Create a new file: User.pm in lib/Parameters/Controller/

First we add the method for handling /api/users:

package Parameters::Controller::User;
use Mojo::Base 'Mojolicious::Controller';

my %users = (
    'bob'  => { name => 'Bob', email => 'bob@eksempel.dk' },
    'alice'=> { name => 'Alice', email => 'alice@eksempel.dk' },
);

sub list {

    # Do not continue on invalid input and render a default 400
    # error document.
    my $c = shift->openapi->valid_input or return;

    my @user_list = ();

    foreach my $user_id (keys %users) {
        my $user = $users{$user_id};
        $user->{userid} = $user_id;
        push @user_list, $user;
    }

    # $output will be validated by the OpenAPI spec before rendered
    my $output = { users => \@user_list };
    $c->render(openapi => $output);
}
Enter fullscreen mode Exit fullscreen mode

We are simulating a model in our application, so the data are just placed in a internal data structure named: %users, this could of course be any sort of model, a database, filesystem or another service.

Now start the application:

$ morbo script/parameters
Enter fullscreen mode Exit fullscreen mode

And finally - lets call the API, do note you do not need jq and your could use curl or httpie, so this is just for sticking to the already available tools, jq being the exception.

$ mojo get http://localhost:3000/api/users | jq
Enter fullscreen mode Exit fullscreen mode

A we get a complete list of our users, currently consisting of Bob and Alice:

{
  "users": [
    {
      "email": "bob@eksempel.dk",
      "name": "Bob",
      "userid": "bob"
    },
    {
      "email": "alice@eksempel.dk",
      "name": "Alice",
      "userid": "alice"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Now lets add the method for handling a single user object via a query parameter:

sub get_by_parameter {

    # Do not continue on invalid input and render a default 400
    # error document.
    my $c = shift->openapi->valid_input or return;

    # $c->openapi->valid_input copies valid data to validation object,
    # and the normal Mojolicious api works as well.
    my $input = $c->validation->output;

    my $id = $input->{'id'};

    my $user = $users{$id};

    if ($user) {
        $user->{userid} = $id;
        # $output will be validated by the OpenAPI spec before rendered
        my $output = { user => $user };
        $c->render(openapi => $output);
    } else {
        $c->respond_to(
            any => { status => 404, json => { message => 'Not found' }}
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Now lets call the API:

$ mojo get http://localhost:3000/api/user?id=alice | jq
Enter fullscreen mode Exit fullscreen mode

And the result for Alice:

{
  "user": {
    "email": "alice@eksempel.dk",
    "name": "Alice",
    "userid": "alice"
  }
}
Enter fullscreen mode Exit fullscreen mode

And finally lets add the method for handling a single user object via the URL:

sub get_by_url {

    # Do not continue on invalid input and render a default 400
    # error document.
    my $c = shift->openapi->valid_input or return;

    my $id = $c->stash('id');

    my $user = $users{$id};

    if ($user) {
        $user->{userid} = $id;
        # $output will be validated by the OpenAPI spec before rendered
        my $output = { user => $user };
        $c->render(openapi => $output);
    } else {
        $c->respond_to(
            any => { status => 404, json => { message => 'Not found' }}
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Lets call the API again using the URL:

$ mojo get http://localhost:3000/api/user/bob | jq
Enter fullscreen mode Exit fullscreen mode

And we get the result for Bob:

{
  "user": {
    "email": "bob@eksempel.dk",
    "name": "Bob",
    "userid": "bob"
  }
}
Enter fullscreen mode Exit fullscreen mode

Yay! our Mojolicious OpenAPI implementation works and we can even support different URL schemas.

Since we take parameters, we need to do one last thing. And that is sanitizing our input. This part is not essential for get going with Mojolicious OpenAPI integration and Mojolicious::Plugin::OpenAPI.

This next part is not required for understanding handling parameters, but if you want get into validation, which is quite essential please read along.

Lets add validation to our two end-points processing data. The Mojolicious::Plugin::OpenAPI already has a hook for validation, but we need to extend this.

Lets add a method to our Parameters::Controller::User. Our id no matter how we receive it has to adhere to the same validation so we add this basic validation method, which overwrites the existing validation for our controller.

sub _validate_id {
    my ($c, $id) = @_;

    my $validator = $c->validation->validator;
    my $validation = $validator->validation;
    $validation->input({id => $id});
    $validation->required('id')->like(qr/^[A-Z]/i);
    $c->validation->validator($validation);

    return $c;
}
Enter fullscreen mode Exit fullscreen mode

As you can see our two end-points fetching users are pretty similar, the only difference is how the id parameter is received, so lets generalise this with the following method:

sub _proces_request {
    my ($c, $id) = @_;

    $c->_validate_id($id);

    if (not $c->validation->validator->has_error) {

        my $input = $c->validation->validator->output;

        my $id = $input->{'id'};
        my $user = $users{$id};

        if ($user) {
            $user->{userid} = $id;
            # $output will be validated by the OpenAPI spec before rendered
            my $output = { user => $user };
            $c->render(openapi => $output);
        } else {
            $c->respond_to(
                any => { status => 404, json => { message => 'Not found' }}
            );
        }
    } else {
        $c->respond_to(
            any => { status => 400, json => { message => 'Bad request' }}
        );
    }

    return $c;
}
Enter fullscreen mode Exit fullscreen mode

We let this method call our validation method, so all we need to do is let the two end-points extract the id parameter and call the _proces_request method:

First: get_by_parameter:

sub get_by_parameter {

    # Do not continue on invalid input and render a default 400
    # error document.
    my $c = shift->openapi->valid_input or return;

    # $c->openapi->valid_input copies valid data to validation object,
    # and the normal Mojolicious api works as well.

    $c->_proces_request($c->param('id'));
}
Enter fullscreen mode Exit fullscreen mode

Secondly: get_by_parameter:

sub get_by_url {

    # Do not continue on invalid input and render a default 400
    # error document.
    my $c = shift->openapi->valid_input or return;

    # $c->openapi->valid_input copies valid data to validation object,
    # and the normal Mojolicious api works as well.

    $c->_proces_request($c->stash('id'));
}
Enter fullscreen mode Exit fullscreen mode

And we should be good to go. The complete controller component should look like the following:

package Parameters::Controller::User;
use Mojo::Base 'Mojolicious::Controller';

my %users = (
    'bob'  => { name => 'Bob', email => 'bob@eksempel.dk' },
    'alice'=> { name => 'Alice', email => 'alice@eksempel.dk' },
);

sub list {

    # Do not continue on invalid input and render a default 400
    # error document.
    my $c = shift->openapi->valid_input or return;

    my @user_list = ();

    foreach my $user_id (keys %users) {
        my $user = $users{$user_id};
        $user->{userid} = $user_id;
        push @user_list, $user;
    }

    # $output will be validated by the OpenAPI spec before rendered
    my $output = { users => \@user_list };
    $c->render(openapi => $output);
}

sub get_by_parameter {

    # Do not continue on invalid input and render a default 400
    # error document.
    my $c = shift->openapi->valid_input or return;

    # $c->openapi->valid_input copies valid data to validation object,
    # and the normal Mojolicious api works as well.

    $c->_proces_request($c->param('id'));
}

sub get_by_url {

    # Do not continue on invalid input and render a default 400
    # error document.
    my $c = shift->openapi->valid_input or return;

    # $c->openapi->valid_input copies valid data to validation object,
    # and the normal Mojolicious api works as well.

    $c->_proces_request($c->stash('id'));
}

sub _proces_request {
    my ($c, $id) = @_;

    $c->_validate_id($id);

    if (not $c->validation->validator->has_error) {

        my $input = $c->validation->validator->output;

        my $id = $input->{'id'};
        my $user = $users{$id};

        if ($user) {
            $user->{userid} = $id;
            # $output will be validated by the OpenAPI spec before rendered
            my $output = { user => $user };
            $c->render(openapi => $output);
        } else {
            $c->respond_to(
                any => { status => 404, json => { message => 'Not found' }}
            );
        }
    } else {
        $c->respond_to(
            any => { status => 400, json => { message => 'Bad request' }}
        );
    }

    return $c;
}

sub _validate_id {
    my ($c, $id) = @_;

    my $validator = $c->validation->validator;
    my $validation = $validator->validation;
    $validation->input({id => $id});
    $validation->required('id')->like(qr/^[A-Z]/i);
    $c->validation->validator($validation);

    return $c;
}

1;
Enter fullscreen mode Exit fullscreen mode

To checkout our new validations try different variations. As we implemented we only allow lettes for user-id and hence we regard the request as a bad request

$ mojo get --verbose http://localhost:3000/api/user/123 | jq
GET /api/user/123 HTTP/1.1
Host: localhost:3000
Accept-Encoding: gzip
User-Agent: Mojolicious (Perl)
Content-Length: 0

HTTP/1.1 400 Bad Request
Date: Fri, 27 Jul 2018 08:25:40 GMT
Content-Type: application/json;charset=UTF-8
Server: Mojolicious (Perl)
Content-Length: 25

{
  "message": "Bad request"
}
Enter fullscreen mode Exit fullscreen mode

And of the other end-point also a bad request:

$ mojo get --verbose http://localhost:3000/api/user?id=123 | jq
GET /api/user?id=123 HTTP/1.1
Host: localhost:3000
User-Agent: Mojolicious (Perl)
Accept-Encoding: gzip
Content-Length: 0

HTTP/1.1 400 Bad Request
Date: Fri, 27 Jul 2018 08:27:24 GMT
Server: Mojolicious (Perl)
Content-Type: application/json;charset=UTF-8
Content-Length: 25

{
  "message": "Bad request"
}
Enter fullscreen mode Exit fullscreen mode

That concludes this part. Have fun experimenting with Mojolicious::Plugin::OpenAPI.

References

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .