Preskočiť na obsah

Príklad: OpenAPI a konzola ​

Malé API knižnice kníh zdokumentované tromi spôsobmi: GET /api/v1/openapi, GET /api/v1/endpoints a webová konzola na /console.

Otvoriť diagram v plnej veľkosti

Služby ​

yaml
application:
	mapping:
		Api: Tomaj\NetteApi\Presenters\*Presenter
		*: App\UI\*\**Presenter

latte:
	extensions:
		# the {apiLink} tag
		- Tomaj\NetteApi\Link\ApiLinkExtension

parameters:
	apiTokens:
		secret-token: '*'

services:
	router: App\RouterFactory::create
	apiPresenter:
		factory: Tomaj\NetteApi\Presenters\ApiPresenter
	- App\UI\Console\ConsolePresenter
	- Tomaj\NetteApi\Output\Configurator\EnvConfigurator
	- Tomaj\NetteApi\Error\DefaultErrorHandler
	- Tomaj\NetteApi\Misc\IpDetector

	apiLink: Tomaj\NetteApi\Link\ApiLink
	# {apiLink} calls the filter of this name
	latte.latteFactory:
		setup:
			- addFilter(apiLink, [@apiLink, link])

	- App\Api\BookListHandler
	- App\Api\BookCreateHandler
	# the listing handlers need ApiDecider, so they are registered by class name below
	# and created only when their endpoint is called
	- Tomaj\NetteApi\Handlers\OpenApiHandler(initData: [info: [title: 'Bookshelf API', description: 'Example API documented with OpenAPI']])
	- Tomaj\NetteApi\Handlers\ApiListingHandler

	apiDecider:
		factory: Tomaj\NetteApi\ApiDecider
		setup:
			- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'books'), App\Api\BookListHandler, Tomaj\NetteApi\Authorization\NoAuthorization())
			- addApi(Tomaj\NetteApi\EndpointIdentifier('POST', 1, 'books'), App\Api\BookCreateHandler, Tomaj\NetteApi\Authorization\BearerTokenAuthorization(Tomaj\NetteApi\Misc\StaticTokenRepository(%apiTokens%)))
			# GET /api/v1/openapi, ?format=yaml with symfony/yaml installed
			- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'openapi'), Tomaj\NetteApi\Handlers\OpenApiHandler, Tomaj\NetteApi\Authorization\NoAuthorization())
			# GET /api/v1/endpoints, a short JSON list of the version 1 endpoints
			- addApi(Tomaj\NetteApi\EndpointIdentifier('GET', 1, 'endpoints'), Tomaj\NetteApi\Handlers\ApiListingHandler, Tomaj\NetteApi\Authorization\NoAuthorization())

Zdokumentovaný handler ​

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Output\JsonOutput;
use Tomaj\NetteApi\Params\GetInputParam;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;
use Tomaj\NetteApi\Validation\InputType;

final class BookListHandler extends BaseHandler
{
    private const BOOKS = [
        ['id' => 1, 'title' => 'Dune', 'year' => 1965],
        ['id' => 2, 'title' => 'Neuromancer', 'year' => 1984],
    ];

    // summary, description and tags end up in the OpenAPI document and the console
    public function summary(): string
    {
        return 'List books';
    }

    public function description(): string
    {
        return 'Returns books published in the given year or later.';
    }

    public function tags(): array
    {
        return ['books'];
    }

    public function params(): array
    {
        return [
            (new GetInputParam('since', InputType::INTEGER))
                ->setDescription('Earliest publication year')
                ->addExample('eighties', 1980),
        ];
    }

    public function handle(array $params): ResponseInterface
    {
        $books = array_values(array_filter(self::BOOKS, fn (array $book): bool => $book['year'] >= ($params['since'] ?? 0)));
        return new JsonApiResponse(200, ['books' => $books]);
    }

    public function outputs(): array
    {
        $schema = [
            'type' => 'object',
            'properties' => [
                'books' => [
                    'type' => 'array',
                    'items' => [
                        'type' => 'object',
                        'properties' => [
                            'id' => ['type' => 'integer'],
                            'title' => ['type' => 'string'],
                            'year' => ['type' => 'integer'],
                        ],
                        'required' => ['id', 'title', 'year'],
                    ],
                ],
            ],
            'required' => ['books'],
        ];
        return [
            (new JsonOutput(200, (string) json_encode($schema), 'Books'))
                ->addExample('two books', ['books' => self::BOOKS]),
        ];
    }
}

Summary, popis, tagy, parametre aj výstupy skončia v OpenAPI dokumente.

Konzola ​

php
<?php

declare(strict_types=1);

namespace App\UI\Console;

use Nette\Application\UI\Presenter;
use Tomaj\NetteApi\ApiDecider;
use Tomaj\NetteApi\Component\ApiConsoleControl;
use Tomaj\NetteApi\Component\ApiListingControl;
use Tomaj\NetteApi\Link\ApiLink;

/**
 * The web console: a list of all endpoints and a generated form to call one.
 * Protect it (or leave it out) in production, it can call every endpoint.
 */
final class ConsolePresenter extends Presenter
{
    private string $method = '';
    private string $version = '';
    private string $package = '';
    private ?string $apiAction = null;

    public function __construct(private ApiDecider $apiDecider, private ApiLink $apiLink)
    {
        parent::__construct();
    }

    public function actionShow(string $method, string $version, string $package, ?string $apiAction = null): void
    {
        $this->method = $method;
        $this->version = $version;
        $this->package = $package;
        $this->apiAction = $apiAction;
        $this->template->endpoint = $this->apiDecider->getApi($method, $version, $package, $apiAction)->getEndpoint();
    }

    protected function createComponentApiListing(): ApiListingControl
    {
        $listing = new ApiListingControl($this->apiDecider);
        $listing->onClick[] = function (string $method, string $version, string $package, ?string $apiAction): void {
            $this->redirect('show', $method, $version, $package, $apiAction);
        };
        return $listing;
    }

    protected function createComponentApiConsole(): ApiConsoleControl
    {
        $api = $this->apiDecider->getApi($this->method, $this->version, $this->package, $this->apiAction);
        return new ApiConsoleControl(
            $this->getHttpRequest(),
            $api->getEndpoint(),
            $api->getHandler(),
            $api->getAuthorization(),
            $this->apiLink,
        );
    }
}

Pohľad default (default.latte) vykreslí zoznam endpointov. Klik na endpoint presmeruje na actionShow, ktorá vykreslí show.latte s formulárom konzoly pre daný endpoint.

latte
{block content}
<h1>API console</h1>
<p>
    OpenAPI document: <a href="{apiLink 'GET', '1', 'openapi'}">JSON</a>,
    <a href="{apiLink 'GET', '1', 'openapi', null, ['format' => 'yaml']}">YAML</a>
</p>
{control apiListing}
{/block}
latte
{block content}
<p><a n:href="default">All endpoints</a></p>
<h1>{$endpoint->getMethod()} /{$endpoint->getUrl()}</h1>
{control apiConsole}
{/block}

Test ​

bash
#!/usr/bin/env bash
# End-to-end check: the OpenAPI document, the endpoint listing and the web console.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f vendor/autoload.php ]] || { echo "fail: vendor/ is missing, run composer install first" >&2; exit 1; }
source ../ci/lib.sh
serve 8085

request "$BASE/api/v1/openapi"
expect_status 200 "OpenAPI JSON"
expect_body '"openapi":"3.0.0"' "OpenAPI JSON"
expect_body '"title":"Bookshelf API"' "OpenAPI JSON"
expect_body '"summary":"List books"' "OpenAPI JSON"
expect_body '"Bearer":{"type":"http","scheme":"bearer"}' "OpenAPI JSON"
ok "GET /api/v1/openapi returns an OpenAPI 3.0 document"

request "$BASE/api/v1/openapi?format=yaml"
expect_status 200 "OpenAPI YAML"
expect_body 'openapi: 3.0.0' "OpenAPI YAML"
ok "GET /api/v1/openapi?format=yaml returns YAML"

request "$BASE/api/v1/endpoints"
expect_status 200 "listing"
expect_body '"method":"POST"' "listing"
ok "GET /api/v1/endpoints lists the endpoints"

request "$BASE/console"
expect_status 200 "console listing"
expect_body '/api/v1/openapi?format=yaml' "{apiLink} in the template"
expect_body 'v1/books/' "console listing"
ok "GET /console lists the endpoints"

jar="$(mktemp)"
# the cookie jar keeps the _nss cookie of Nette forms, which accept a POST only from the same site
request -c "$jar" -b "$jar" "$BASE/console/show/GET/1/books"
expect_status 200 "console form"
expect_body 'name="since"' "console form"
ok "GET /console/show/GET/1/books renders the form for the since parameter"

request -c "$jar" -b "$jar" -d "api_url=$BASE/api/v1/books/&api_method=GET&since=1980&timeout=10&_do=apiConsole-consoleForm-submit&send=1" "$BASE/console/show/GET/1/books"
rm -f "$jar"
expect_status 200 "console request"
expect_body 'Neuromancer' "console request"
ok "submitting the console form calls the endpoint and shows the response"

Časté otázky ​

Prečo je dostupný YAML? ​

Príklad vyžaduje symfony/yaml. Bez neho je povolený len ?format=json. Pozrite si OpenAPI.

Ako dokument vie, že POST /books potrebuje token? ​

Prečíta BearerTokenAuthorization a pridá bezpečnostnú schému Bearer.

Má byť konzola verejná? ​

Nie, chráňte ju, alebo ju v produkcii vynechajte. Pozrite si Webovú konzolu.

Latte rozšírenie a filter apiLink, oboje v konfigurácii vyššie. Pozrite si Odkazy.

Nette formuláre pred prijatím POST kontrolujú same-site cookie _nss.

Vydané pod licenciou MIT.