Digging Deeper

OpenAPI Documentation

Document every endpoint of your resources in OpenAPI with Scramble, with typed bodies, responses and examples, and keep the document under test.

The package documents its endpoints through Scramble. It builds the document from the declarations of your resources: fields, relations, scopes, instructions, actions, metrics, limits and gates.

Installation

composer require dedoc/scramble:^0.13.48

The package registers its documentation in the boot() method of its service provider: config/scramble.php needs no change. Visit /docs/api to read the document, or export it:

php artisan scramble:export

An API that your application registers with Scramble::registerApi() after the package boots gets the package documentation too. For an API registered before, add the transformers:

Scramble::registerApi('v2', ['api_path' => 'api/v2'])
    ->withOperationTransformers(\Lomkit\Rest\Scramble\RestOperations::class)
    ->withDocumentTransformers(\Lomkit\Rest\Scramble\RestDocument::class);

What the document holds

The document is the same for every reader. The package reads the declarations of each resource as a guest: a field, an action or a metric declared for some users only is documented as a guest sees it.

OperationSummaryBodyResponse
GET /{resource}Get {Resource} detailsthe with parameter{Resource}Details
POST /{resource}/searchSearch {Resource}{Resource}Search{Resource}Paginated
POST /{resource}/mutateMutate {Resource}{Resource}Mutation entries{Resource}MutateResult
POST /{resource}/actions/{uriKey}the name of the actionsearch or resources, as the target requires, and fieldsRestActionResponse
POST /{resource}/metrics/{uriKey}the name of the metricsearch, range, timezonedata with the result of the metric
DELETE /{resource}, POST /{resource}/restore, DELETE /{resource}/forceDelete {Resource}, Restore {Resource}, Force delete {Resource}resources{Resource}Rows
GET /{resource}/actions/{action}/batches/{batch}Batch progress of {Resource} actionsRestBatchProgress

{Resource} is the class name of the resource without its Resource suffix, also used as the tag.

  • Records. Each resource has a record, named like its tag (Post): its selectable fields with their type, the gates of a row, and each relation under its key. The morph type column of a MorphTo relation lists its morph types as an enum. A MorphTo relation is oneOf the record of each target, with a discriminator on @type.
  • Search. Filters list one condition per filterable field, with the operators of the field and the value that each one takes, plus the fields of each related resource one level deep and the pivot fields. Includes, aggregates, scopes and instructions have one schema per relation or name. Lists hold at most their rest.limits.sizes entries. sorts lists the key of each aggregate that sorts: an aggregate over a relation to many, with no field or on a field that is not a key, see Sorts and selects.
  • Mutations. attributes lists the fields that the operation writes, with their rules in the description. An Enum field lists the cases that writes accept. Each relation lists the operations that it accepts.
  • Actions. The body follows the target of the action: search is required on the Search target, resources on the Resources target. A key that the target refuses is documented as false. fields lists typed {name, value} entries. The result is oneOf the types of ActionResponse, with a discriminator on type. A batched action answers RestBatchableActionResponse, with batch. See Actions and writes.
  • Metrics. The built-in types share RestValueResult, RestTrendResult, RestPartitionResult (with previous on each part and on other when the partition compares two periods), RestTableResult (with percentile on a percentile column) and RestProgressResult. parts and splits hold at most rest.limits.sizes.groups entries; a partition or a trend with its own limit() has its schema, with that maximum, in its operation. A metric of your own documents its type alone until it declares schema(), see Metrics.
  • Details. {Resource}Details lists the names of the resource as enums. The base keys are required, the sections and parts optional. meta accepts any key when the resource overrides detailsMeta().

Actions and writes

{
  "/shows/actions/cancel-shows": {
    "post": {
      "description": "The action is destructive: a hint for clients, the endpoint runs it the same way.\n\nThe text that a client shows before it runs the action: Cancel the selected shows?\n\nThe action can answer 428: send the same body again with the token of the answer in `confirmation`.",
      "summary": "Cancel Shows",
      "x-lomkit-destructive": true,
      "x-lomkit-confirm": "Cancel the selected shows?"
    }
  }
}
  • The operation of a destructive() action carries "x-lomkit-destructive": true, and the operation of an action with confirm() carries its text under x-lomkit-confirm. Its description says both.
  • An action that declares confirmation() documents confirmation in its body and the 428 response. So do the mutate, destroy, restore and forceDestroy operations of a resource that declares the matching hook, see Confirmation.
  • A key that an operation refuses is written "<key>": false in its properties, such as "search": false on the Resources target, "range": false on a metric without ranges, or "text": false on a search of a resource without a text search: the endpoint answers 422 when the request sends it.

Errors

components.responses holds the shared error responses, listed on the operations of the package:

ResponseStatusListed on
RestUnauthenticated401the operations whose route runs auth
RestForbidden403every operation; with denials when the application registers DenialsRenderer
RestNotFound404every operation
RestConflict409the mutate operations that write a versioned resource, at the root or under a relation
RestValidationFailed422every operation but batch progress
RestConfirmationRequired428the actions that declare confirmation(), and the writes of a resource that declares the matching confirmation hook

An operation whose route does not run auth has "security": []. The package declares no authentication scheme: declare yours in a document transformer.

use Dedoc\Scramble\Scramble;
use Dedoc\Scramble\Support\Generator\OpenApi;
use Dedoc\Scramble\Support\Generator\SecurityScheme;

Scramble::configure()->withDocumentTransformers(function (OpenApi $openApi): void {
    $openApi->secure(SecurityScheme::http('bearer'));
});

Types and examples

Each field documents its type:

FieldSchema
IDan integer or a string, from the key type
Textstring
Number, Integernumber, integer
Booleanboolean
Date, DateTimestring with format: date or format: date-time
Enuman enum of its backing values
Jsonobject

nullable() adds null, and meta() goes under x-lomkit-meta. A field type of your own overrides toSchema(), and toWriteSchema() when writes accept fewer values, as an Enum with offered() does.

  • The responses follow these types: a Number field answers a number, even from a decimal cast, and an empty Json field answers {}. See Fields.
  • The rules of a field go in its description, such as Rules: string, max:80, required, on the fields of mutate, of actions, of instructions and of scopes.
  • An action field, an instruction field, a scope parameter or a metric range with a default gives it under default, such as "default": true or "default": 30.

examples() lists example values on a field. The request examples of each operation use the first value:

Text::make('title')->examples('Weekly report', 'Daily report'),

Without examples(), an example takes a value of the field's type that passes its simple rules: the length of min, max, size and between on a string (aaaaaaaa for min:8), their bounds and those of gt, gte, lt and lte on a number, the first value of in, and a value for email, url, uuid, ulid, ip, date, numeric, digits, accepted and declined.

  • A request example never sends a default: a field with a default is left out of an action example, and a metric example is {}.
  • The mutate example adds each relation declared requiredOnCreation(), with an attach of a key.
Declare examples() on the fields whose other rules the generated value does not pass, so that the request examples pass your rules.
The date cast of Laravel renders a date and a time. Cast a Date attribute with date:Y-m-d so that the response matches the documented format: date.

Keeping the document under test

The package tests its own document in two ways, and you can do the same in your application.

  • Contract tests. The package sends real requests to each documented operation and validates each response against the schema of the document: a response that does not match fails the test.
  • A snapshot. The package generates the document of a fixture application and compares it with a committed JSON file. A change of a declaration shows as a diff of that file.

A snapshot test in your application can compare the generated document with a committed file:

use Dedoc\Scramble\Generator;

public function test_the_api_document_matches_the_snapshot(): void
{
    $document = app(Generator::class)();
    unset($document['info'], $document['servers']);

    $json = json_encode($document, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)."\n";

    $this->assertSame(file_get_contents(base_path('tests/api.json')), $json);
}

Write the file once from the same code, and commit it. When a change to a resource changes the API, the test fails and the diff of tests/api.json shows what clients see.