feat(api-docs): document binary pixel responses
This commit is contained in:
parent
8e28b0209c
commit
6ea0b26a13
4 changed files with 61 additions and 2 deletions
|
|
@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace Core\Api\Controllers\Api;
|
||||
|
||||
use Core\Api\Documentation\Attributes\ApiResponse;
|
||||
use Core\Api\Documentation\Attributes\ApiTag;
|
||||
use Core\Api\RateLimit\RateLimit;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
|
|
@ -15,6 +17,7 @@ use Illuminate\Routing\Controller;
|
|||
* GET /api/pixel/{pixelKey} returns a transparent 1x1 GIF for image embeds.
|
||||
* POST /api/pixel/{pixelKey} returns 204 No Content for fetch-based tracking.
|
||||
*/
|
||||
#[ApiTag('Pixel', 'Unified tracking pixel endpoint')]
|
||||
class UnifiedPixelController extends Controller
|
||||
{
|
||||
/**
|
||||
|
|
@ -28,6 +31,17 @@ class UnifiedPixelController extends Controller
|
|||
* GET /api/pixel/abc12345 -> transparent GIF
|
||||
* POST /api/pixel/abc12345 -> 204 No Content
|
||||
*/
|
||||
#[ApiResponse(
|
||||
200,
|
||||
null,
|
||||
'Transparent 1x1 GIF pixel response',
|
||||
contentType: 'image/gif',
|
||||
schema: [
|
||||
'type' => 'string',
|
||||
'format' => 'binary',
|
||||
],
|
||||
)]
|
||||
#[ApiResponse(204, null, 'Accepted without a response body')]
|
||||
#[RateLimit(limit: 10000, window: 60)]
|
||||
public function track(Request $request, string $pixelKey): Response
|
||||
{
|
||||
|
|
|
|||
|
|
@ -27,6 +27,19 @@ use Attribute;
|
|||
* {
|
||||
* return UserResource::collection(User::paginate());
|
||||
* }
|
||||
*
|
||||
* // For non-JSON or binary responses
|
||||
* #[ApiResponse(
|
||||
* 200,
|
||||
* null,
|
||||
* 'Transparent tracking pixel',
|
||||
* contentType: 'image/gif',
|
||||
* schema: ['type' => 'string', 'format' => 'binary']
|
||||
* )]
|
||||
* public function pixel()
|
||||
* {
|
||||
* return response($gif, 200)->header('Content-Type', 'image/gif');
|
||||
* }
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
readonly class ApiResponse
|
||||
|
|
@ -37,6 +50,8 @@ readonly class ApiResponse
|
|||
* @param string|null $description Description of the response
|
||||
* @param bool $paginated Whether this is a paginated collection response
|
||||
* @param array<string> $headers Additional response headers to document
|
||||
* @param string|null $contentType Explicit response media type for non-JSON responses
|
||||
* @param array<string, mixed>|null $schema Explicit response schema when the body is not inferred from a resource
|
||||
*/
|
||||
public function __construct(
|
||||
public int $status,
|
||||
|
|
@ -44,6 +59,8 @@ readonly class ApiResponse
|
|||
public ?string $description = null,
|
||||
public bool $paginated = false,
|
||||
public array $headers = [],
|
||||
public ?string $contentType = null,
|
||||
public ?array $schema = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -612,15 +612,23 @@ class OpenApiBuilder
|
|||
'description' => $response->getDescription(),
|
||||
];
|
||||
|
||||
if ($response->resource !== null && class_exists($response->resource)) {
|
||||
$schema = null;
|
||||
|
||||
if (is_array($response->schema) && ! empty($response->schema)) {
|
||||
$schema = $response->schema;
|
||||
} elseif ($response->resource !== null && class_exists($response->resource)) {
|
||||
$schema = $this->extractResourceSchema($response->resource);
|
||||
|
||||
if ($response->paginated) {
|
||||
$schema = $this->wrapPaginatedSchema($schema);
|
||||
}
|
||||
}
|
||||
|
||||
if ($schema !== null) {
|
||||
$contentType = $response->contentType ?: 'application/json';
|
||||
|
||||
$result['content'] = [
|
||||
'application/json' => [
|
||||
$contentType => [
|
||||
'schema' => $schema,
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -214,6 +214,26 @@ describe('Application Endpoint Parameter Docs', function () {
|
|||
expect($urlParam['schema']['format'])->toBe('uri');
|
||||
});
|
||||
|
||||
it('documents the pixel endpoint as binary for GET and no-content for POST', function () {
|
||||
$builder = new OpenApiBuilder;
|
||||
$spec = $builder->build();
|
||||
|
||||
$getOperation = $spec['paths']['/api/pixel/{pixelKey}']['get'];
|
||||
$getResponse = $getOperation['responses']['200'] ?? [];
|
||||
$getContent = $getResponse['content']['image/gif']['schema'] ?? null;
|
||||
|
||||
expect($getContent)->toBe([
|
||||
'type' => 'string',
|
||||
'format' => 'binary',
|
||||
]);
|
||||
|
||||
$postOperation = $spec['paths']['/api/pixel/{pixelKey}']['post'];
|
||||
$postResponse = $postOperation['responses']['204'] ?? [];
|
||||
|
||||
expect($postResponse['description'] ?? null)->toBe('Accepted without a response body');
|
||||
expect($postResponse)->not->toHaveKey('content');
|
||||
});
|
||||
|
||||
it('documents MCP list query parameters', function () {
|
||||
$builder = new OpenApiBuilder;
|
||||
$spec = $builder->build();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue