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.
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.
Čo potrebuje {apiLink}?
Latte rozšírenie a filter apiLink, oboje v konfigurácii vyššie. Pozrite si Odkazy.
Prečo si test drží cookie jar?
Nette formuláre pred prijatím POST kontrolujú same-site cookie _nss.