Skip to content

Commit 4740bfa

Browse files
authored
[Feature] Add Schemas to API (alexjustesen#2216)
1 parent c7fc406 commit 4740bfa

19 files changed

+1415
-36
lines changed

app/Http/Controllers/Api/V1/ApiController.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44

55
use Illuminate\Http\Exceptions\HttpResponseException;
66
use Illuminate\Support\Facades\Log;
7-
use OpenApi\Attributes as OA;
87

9-
#[OA\Info(title: 'Speedtest Tracker API', version: '1.0.0')]
108
abstract class ApiController
119
{
1210
/**

app/Http/Controllers/Api/V1/LatestResult.php

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,27 @@ class LatestResult extends ApiController
1313
{
1414
#[OA\Get(
1515
path: '/api/v1/results/latest',
16-
description: 'Get the latest result.',
16+
summary: 'Fetch the single most recent result',
17+
operationId: 'getLatestResult',
18+
tags: ['Results'],
1719
responses: [
18-
new OA\Response(response: 200, description: 'OK'),
19-
new OA\Response(response: Response::HTTP_NOT_FOUND, description: 'No result found'),
20-
])]
20+
new OA\Response(
21+
response: Response::HTTP_OK,
22+
description: 'OK',
23+
content: new OA\JsonContent(ref: '#/components/schemas/Result')
24+
),
25+
new OA\Response(
26+
response: Response::HTTP_UNAUTHORIZED,
27+
description: 'Unauthenticated',
28+
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
29+
),
30+
new OA\Response(
31+
response: Response::HTTP_NOT_FOUND,
32+
description: 'No result found',
33+
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundError')
34+
),
35+
]
36+
)]
2137
public function __invoke(Request $request)
2238
{
2339
$result = Result::query()

app/Http/Controllers/Api/V1/ListResults.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,29 @@ class ListResults extends ApiController
1616
{
1717
#[OA\Get(
1818
path: '/api/v1/results',
19-
description: 'List results.',
19+
summary: 'List results',
20+
operationId: 'listResults',
21+
tags: ['Results'],
2022
responses: [
21-
new OA\Response(response: Response::HTTP_OK, description: 'OK'),
22-
new OA\Response(response: Response::HTTP_UNPROCESSABLE_ENTITY, description: 'Unprocessable Entity'),
23-
])]
23+
new OA\Response(
24+
response: Response::HTTP_OK,
25+
description: 'OK',
26+
content: new OA\JsonContent(
27+
ref: '#/components/schemas/ResultsCollection'
28+
)
29+
),
30+
new OA\Response(
31+
response: Response::HTTP_UNAUTHORIZED,
32+
description: 'Unauthenticated',
33+
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
34+
),
35+
new OA\Response(
36+
response: Response::HTTP_UNPROCESSABLE_ENTITY,
37+
description: 'Validation failed',
38+
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
39+
),
40+
]
41+
)]
2442
public function __invoke(Request $request)
2543
{
2644
$validator = Validator::make($request->all(), [

app/Http/Controllers/Api/V1/ListSpeedtestServers.php

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,30 @@ class ListSpeedtestServers extends ApiController
1111
{
1212
#[OA\Get(
1313
path: '/api/v1/ookla/list-servers',
14-
description: 'Get a list of available Ookla speedtest servers.',
14+
summary: 'List available Ookla speedtest servers',
15+
operationId: 'listSpeedtestServers',
16+
tags: ['Servers'],
1517
responses: [
16-
new OA\Response(response: Response::HTTP_OK, description: 'OK'),
17-
new OA\Response(response: Response::HTTP_FORBIDDEN, description: 'Forbidden'),
18+
new OA\Response(
19+
response: Response::HTTP_OK,
20+
description: 'OK',
21+
content: new OA\JsonContent(
22+
ref: '#/components/schemas/ServersCollection'
23+
)
24+
),
25+
new OA\Response(
26+
response: Response::HTTP_UNAUTHORIZED,
27+
description: 'Unauthenticated',
28+
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
29+
),
30+
new OA\Response(
31+
response: Response::HTTP_FORBIDDEN,
32+
description: 'Forbidden',
33+
content: new OA\JsonContent(
34+
ref: '#/components/schemas/ForbiddenError',
35+
example: ['message' => 'You do not have permission to view speedtest servers.']
36+
)
37+
),
1838
]
1939
)]
2040
public function __invoke(Request $request)

app/Http/Controllers/Api/V1/RunSpeedtest.php

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,39 @@ class RunSpeedtest extends ApiController
1313
{
1414
#[OA\Post(
1515
path: '/api/v1/speedtests/run',
16-
description: 'Run a new Ookla speedtest. Optionally provide a server_id.',
16+
summary: 'Run a new Ookla speedtest',
17+
operationId: 'runSpeedtest',
18+
tags: ['Speedtests'],
1719
parameters: [
1820
new OA\Parameter(
1921
name: 'server_id',
2022
in: 'query',
23+
description: 'Optional Ookla speedtest server ID',
2124
required: false,
22-
schema: new OA\Schema(type: 'integer'),
23-
description: 'Optional Ookla speedtest server ID'
25+
schema: new OA\Schema(type: 'integer')
2426
),
2527
],
2628
responses: [
27-
new OA\Response(response: Response::HTTP_CREATED, description: 'Created'),
28-
new OA\Response(response: Response::HTTP_FORBIDDEN, description: 'Forbidden'),
29-
new OA\Response(response: Response::HTTP_UNPROCESSABLE_ENTITY, description: 'Validation error'),
29+
new OA\Response(
30+
response: Response::HTTP_CREATED,
31+
description: 'Created',
32+
content: new OA\JsonContent(ref: '#/components/schemas/SpeedtestRun')
33+
),
34+
new OA\Response(
35+
response: Response::HTTP_UNAUTHORIZED,
36+
description: 'Unauthenticated',
37+
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
38+
),
39+
new OA\Response(
40+
response: Response::HTTP_FORBIDDEN,
41+
description: 'Forbidden',
42+
content: new OA\JsonContent(ref: '#/components/schemas/ForbiddenError')
43+
),
44+
new OA\Response(
45+
response: Response::HTTP_UNPROCESSABLE_ENTITY,
46+
description: 'Validation error',
47+
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
48+
),
3049
]
3150
)]
3251
public function __invoke(Request $request)

app/Http/Controllers/Api/V1/ShowResult.php

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,36 @@ class ShowResult extends ApiController
1313
{
1414
#[OA\Get(
1515
path: '/api/v1/results/{id}',
16-
description: 'Get result.',
16+
summary: 'Fetch a single result by ID',
17+
operationId: 'getResult',
18+
tags: ['Results'],
19+
parameters: [
20+
new OA\Parameter(
21+
name: 'id',
22+
in: 'path',
23+
required: true,
24+
schema: new OA\Schema(type: 'integer'),
25+
description: 'The ID of the result'
26+
),
27+
],
1728
responses: [
18-
new OA\Response(response: 200, description: 'OK'),
19-
new OA\Response(response: Response::HTTP_NOT_FOUND, description: 'Result not found'),
20-
])]
29+
new OA\Response(
30+
response: Response::HTTP_OK,
31+
description: 'OK',
32+
content: new OA\JsonContent(ref: '#/components/schemas/ResultResponse')
33+
),
34+
new OA\Response(
35+
response: Response::HTTP_UNAUTHORIZED,
36+
description: 'Unauthenticated',
37+
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
38+
),
39+
new OA\Response(
40+
response: Response::HTTP_NOT_FOUND,
41+
description: 'Result not found',
42+
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundError')
43+
),
44+
]
45+
)]
2146
public function __invoke(Request $request, int $id)
2247
{
2348
$result = Result::findOr($id, function () {

app/Http/Controllers/Api/V1/Stats.php

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
use App\Http\Resources\V1\StatResource;
66
use App\Models\Result;
77
use Illuminate\Http\Request;
8+
use Illuminate\Http\Response;
9+
use OpenApi\Attributes as OA;
10+
use OpenApi\Attributes\Schema as OASchema;
811
use Spatie\QueryBuilder\AllowedFilter;
912
use Spatie\QueryBuilder\Enums\FilterOperator;
1013
use Spatie\QueryBuilder\QueryBuilder;
@@ -14,6 +17,45 @@ class Stats extends ApiController
1417
/**
1518
* Handle the incoming request.
1619
*/
20+
#[OA\Get(
21+
path: '/api/v1/stats',
22+
summary: 'Fetch aggregated Speedtest statistics',
23+
operationId: 'getStats',
24+
tags: ['Stats'],
25+
parameters: [
26+
new OA\Parameter(
27+
name: 'start_at',
28+
in: 'query',
29+
description: 'ISO‑8601 start datetime filter',
30+
required: false,
31+
schema: new OASchema(type: 'string', format: 'date-time')
32+
),
33+
new OA\Parameter(
34+
name: 'end_at',
35+
in: 'query',
36+
description: 'ISO‑8601 end datetime filter',
37+
required: false,
38+
schema: new OASchema(type: 'string', format: 'date-time')
39+
),
40+
],
41+
responses: [
42+
new OA\Response(
43+
response: Response::HTTP_OK,
44+
description: 'OK',
45+
content: new OA\JsonContent(ref: '#/components/schemas/Stats')
46+
),
47+
new OA\Response(
48+
response: Response::HTTP_UNAUTHORIZED,
49+
description: 'Unauthenticated',
50+
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
51+
),
52+
new OA\Response(
53+
response: Response::HTTP_UNPROCESSABLE_ENTITY,
54+
description: 'Validation failed',
55+
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
56+
),
57+
]
58+
)]
1759
public function __invoke(Request $request)
1860
{
1961
$stats = QueryBuilder::for(Result::class)

app/OpenApi/OpenApiDefinition.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace App\OpenApi;
4+
5+
use OpenApi\Attributes as OA;
6+
7+
#[OA\OpenApi(
8+
info: new OA\Info(
9+
title: 'Speedtest Tracker API',
10+
version: '1.0.0',
11+
),
12+
components: new OA\Components(
13+
securitySchemes: [
14+
new OA\SecurityScheme(
15+
securityScheme: 'bearerAuth',
16+
type: 'http',
17+
scheme: 'bearer',
18+
bearerFormat: 'JWT'
19+
),
20+
],
21+
parameters: [
22+
new OA\Parameter(
23+
name: 'Accept',
24+
in: 'header',
25+
required: true,
26+
schema: new OA\Schema(type: 'string', default: 'application/json'),
27+
description: 'Expected response format'
28+
),
29+
]
30+
),
31+
tags: [
32+
new OA\Tag(name: 'Results', description: "Operations related to Speedtest results.\nRequires an API token with scope `results:read`."),
33+
new OA\Tag(name: 'Speedtests', description: "Operations to run speedtests.\nRequires an API token with scope `speedtests:run`."),
34+
new OA\Tag(name: 'Servers', description: "Operations for speedtest servers.\nRequires an API token with scope `ookla:list-servers`."),
35+
new OA\Tag(name: 'Stats', description: "Operations for statistics.\nRequires an API token with scope `results:read`."),
36+
]
37+
)]
38+
class OpenApiDefinition {}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\OpenApi\Schemas;
4+
5+
use OpenApi\Attributes as OA;
6+
7+
#[OA\Schema(
8+
schema: 'ForbiddenError',
9+
type: 'object',
10+
description: 'Forbidden error response when user lacks permission',
11+
properties: [
12+
new OA\Property(
13+
property: 'message',
14+
type: 'string',
15+
description: 'Error message indicating lack of permission',
16+
),
17+
]
18+
)]
19+
class ForbiddenErrorSchema {}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace App\OpenApi\Schemas;
4+
5+
use OpenApi\Attributes as OA;
6+
7+
#[OA\Schema(
8+
schema: 'NotFoundError',
9+
type: 'object',
10+
description: 'Error when a requested result is not found',
11+
properties: [
12+
new OA\Property(
13+
property: 'message',
14+
type: 'string',
15+
description: 'Result not found error message',
16+
),
17+
],
18+
)]
19+
class NotFoundErrorSchema {}

0 commit comments

Comments
 (0)