Rand Stats

Cro::OpenAPI::RoutesFromDefinition

cpan:JNTHN

Cro::OpenAPI::RoutesFromDefinition Build Status

Takes an existing OpenAPI Document and allows straightforward implementation of the API defined within it using the Cro libraries.

Synopsis

# Implement the OpenAPI defined in schema.yaml.
my $routes = openapi 'schema.yaml'.IO, {
    # Given an operation defined like this:
    # 
    #   summary: Updates a pet in the store with form data
    #   operationId: updatePetWithForm
    #   parameters:
    #   - name: petId
    #     in: path
    #     description: ID of pet that needs to be updated
    #     required: true
    #     schema:
    #   	type: string
    #   requestBody:
    #     content:
    #   	'application/x-www-form-urlencoded':
    #   	  schema:
    #   	   properties:
    #   		  name: 
    #   			description: Updated name of the pet
    #   			type: string
    #   		  status:
    #   			description: Updated status of the pet
    #   			type: string
    #   	   required:
    #   		 - status
    #   responses:
    #     '200':
    #   	description: Pet updated.
    #   	content: 
    #   	  'application/json': {}
    #     '400':
    #   	description: Invalid input
    #   	content: 
    #   	  'application/json': {}
    #
    # We can implement it by receiving the route parameter as a positional
    # argument; other literal route segments need not be mentioned.
    operation 'updatePetWithForm', -> $id {
        # The request body will already have been validated, so just grab
        # it, perhaps using destructuring.
        request-body -> (:$name, :$status) {
            # Do something with it.
            $some-store.update-pet($id, $name, $status);

            # Respond (response automatically checked against schema too).
            content 'application/json', {};
        }
    }
}

The $routes object is a subclass of Cro::HTTP::Router::RouteSet, and so can be included into a route block:

my $api-routes = openapi 'schema.yaml'.IO, {
    ...
}
my $app = route {
    include 'api' => $api-routes;
}

Since it is also a Cro::Transform, then it may be hosted directly as the application using Cro::HTTP::Server.

my $service = Cro::HTTP::Server:
    :host<0.0.0.0>, :port(10000),
    :application($api-routes);

The openapi sub

The openapi sub works somewhat like route from Cro::HTTP::Router. As in a route block, it is possible to:

By contrast, get, put, post and so forth are not valid in the context of an openapi block, and using them will produce an error. Instead, the operation sub should be used to specify the implementations of operations defined by the OpenAPI document. The URI patterns to match will be taken from the OpenAPI document, and need not be repeated. Similarly, include and delegate are not available either (a form of include may be supported in the future in order to allow for breaking up the definition of a large API over multiple files).

The openapi sub may be passed a string containing an OpenAPI document in either YAML or JSON:

openapi $json-doc, {
    ...
}

Or an IO object pointing to a file to read the document from:

openapi "api.yaml".IO, {
    ...
}

In either case, JSON will be detected by looking at the data that is read and seeing if it starts with { (with leading whitespace allowed); failing that, it will be parsed as YAML.

The openapi sub may be passed the following options:

All operations in the OpenAPI document should have an operationId in order to be implementable. Unless configured with :ignore-unimplemented, such operations will be complained about, with a note that it is not even possible to implement them.

The operation sub

The operation sub is used to specify the implementation of an operation in the OpenAPI file. It takes a string operation ID and a block that will be run per request to that operation.

If the string operation ID does not match an operationId in the OpenAPI definition, an error will be raised.

The signature of the block may be used in order to unpack various properties of the request. This works similarly to signatures on get and similar in Cro::HTTP::Router, but with some differences.

Otherwise, it is just like being inside a normal get, post, etc. block as with Cro::HTTP::Router. The request and response terms provide access to the request and response objects, the request-body sub is available, and the various response helpers (such as content) are also available.

Automatic Validation

A request will be validated against the OpenAPI definition. The following aspects of the request will be validated:

A response will (unless response validation is disabled) be validated for:

Failure to validate the response indicates an implementation error. A 500 error will be returned to the client, and the error will be logged.

Manually handling request validation errors

It may in some cases be desirable to handle request validation errors as part of the operation implementation. Note that this does not apply to an incorrect method or non-matching route parameters. Further, it presumes that any named unpacks in the operation signature are liberal enough to cope with the invalid data.

To manually handle request validation errors, pass :allow-invalid to the operation sub. The request-validation-error sub can then be used in order to check if there is a validation error. If there is, then it will be populated with an instance of X::Cro::OpenAPI::RoutesFromDefinition::CheckFailed, which is a subclass of Exception. It has the properties:

If there is no request validation error, then Nil is returned, meaning it can be tested using with or without.

operation 'foo', :allow-invalid, -> $path-param {
    with request-validation-error() -> $error {
        content 'application/json', { :result('error'), :reason($error.reason) };
    }
    else {
        ???;
        content 'application/json', { :result("ok") };
    }
}

Security requirements

Enforcing security requirements involves:

Implementing the role requires implementing a single method, which receives the security scheme to enforce, the HTTP request object, an array of requirements (optional, and only applicable to OpenID) and the operation ID (also optional). It should return True if the requester satisfies the security requirements, and False if not.

role Cro::OpenAPI::RoutesFromDefinition::SecurityChecker {
    method is-allowed(OpenAPI::Model::SecurityScheme $scheme, Cro::HTTP::Request $request,
            :@requirements, :$operation-id --> Bool) { ... }
}

For the case of API keys, the role provides a get-api-key($scheme, $request) method that will use the scheme to look up the API key from the request. It will return a Failure if the is no such header, cookie, or query string parameter, or if the scheme type is not apiKey.

An example implementation of the role looks like this:

class KeyChecker does Cro::OpenAPI::RoutesFromDefinition::SecurityChecker {
    method is-allowed(OpenAPI::Model::SecurityScheme $scheme, Cro::HTTP::Request $request --> Bool) {
        with self.get-api-key($scheme, $request) -> $key {
            if $key.starts-with('totally-legit') {
                $request.auth = MyAuthInfo.new(:$key);
                return True;
            }
        }
        return False;
    }
}

Which could be used like this:

my $application = openapi $api-doc, security => KeyChecker, {
    operation 'public', -> {
        content 'text/plain', 'public ok';
    }
    operation 'private', -> {
        content 'text/plain', 'private ok, key=' ~ request.auth.key;
    }
}

Author

Jonathan Worthington jonathan@edument.se

Copyright 2018 Edument Central Europe sro.

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.