Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions app/Http/Controllers/Api/V1/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Support\Facades\Log;
use OpenApi\Attributes as OA;

#[OA\Info(title: 'Speedtest Tracker API', version: '1.0.0')]
abstract class ApiController
{
/**
Expand Down
24 changes: 20 additions & 4 deletions app/Http/Controllers/Api/V1/LatestResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,27 @@ class LatestResult extends ApiController
{
#[OA\Get(
path: '/api/v1/results/latest',
description: 'Get the latest result.',
summary: 'Fetch the single most recent result',
operationId: 'getLatestResult',
tags: ['Results'],
responses: [
new OA\Response(response: 200, description: 'OK'),
new OA\Response(response: Response::HTTP_NOT_FOUND, description: 'No result found'),
])]
new OA\Response(
response: Response::HTTP_OK,
description: 'OK',
content: new OA\JsonContent(ref: '#/components/schemas/Result')
),
new OA\Response(
response: Response::HTTP_UNAUTHORIZED,
description: 'Unauthenticated',
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
),
new OA\Response(
response: Response::HTTP_NOT_FOUND,
description: 'No result found',
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundError')
),
]
)]
public function __invoke(Request $request)
{
$result = Result::query()
Expand Down
26 changes: 22 additions & 4 deletions app/Http/Controllers/Api/V1/ListResults.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,29 @@ class ListResults extends ApiController
{
#[OA\Get(
path: '/api/v1/results',
description: 'List results.',
summary: 'List results',
operationId: 'listResults',
tags: ['Results'],
responses: [
new OA\Response(response: Response::HTTP_OK, description: 'OK'),
new OA\Response(response: Response::HTTP_UNPROCESSABLE_ENTITY, description: 'Unprocessable Entity'),
])]
new OA\Response(
response: Response::HTTP_OK,
description: 'OK',
content: new OA\JsonContent(
ref: '#/components/schemas/ResultsCollection'
)
),
new OA\Response(
response: Response::HTTP_UNAUTHORIZED,
description: 'Unauthenticated',
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
),
new OA\Response(
response: Response::HTTP_UNPROCESSABLE_ENTITY,
description: 'Validation failed',
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
),
]
)]
public function __invoke(Request $request)
{
$validator = Validator::make($request->all(), [
Expand Down
26 changes: 23 additions & 3 deletions app/Http/Controllers/Api/V1/ListSpeedtestServers.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,30 @@ class ListSpeedtestServers extends ApiController
{
#[OA\Get(
path: '/api/v1/ookla/list-servers',
description: 'Get a list of available Ookla speedtest servers.',
summary: 'List available Ookla speedtest servers',
operationId: 'listSpeedtestServers',
tags: ['Servers'],
responses: [
new OA\Response(response: Response::HTTP_OK, description: 'OK'),
new OA\Response(response: Response::HTTP_FORBIDDEN, description: 'Forbidden'),
new OA\Response(
response: Response::HTTP_OK,
description: 'OK',
content: new OA\JsonContent(
ref: '#/components/schemas/ServersCollection'
)
),
new OA\Response(
response: Response::HTTP_UNAUTHORIZED,
description: 'Unauthenticated',
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
),
new OA\Response(
response: Response::HTTP_FORBIDDEN,
description: 'Forbidden',
content: new OA\JsonContent(
ref: '#/components/schemas/ForbiddenError',
example: ['message' => 'You do not have permission to view speedtest servers.']
)
),
]
)]
public function __invoke(Request $request)
Expand Down
31 changes: 25 additions & 6 deletions app/Http/Controllers/Api/V1/RunSpeedtest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,39 @@ class RunSpeedtest extends ApiController
{
#[OA\Post(
path: '/api/v1/speedtests/run',
description: 'Run a new Ookla speedtest. Optionally provide a server_id.',
summary: 'Run a new Ookla speedtest',
operationId: 'runSpeedtest',
tags: ['Speedtests'],
parameters: [
new OA\Parameter(
name: 'server_id',
in: 'query',
description: 'Optional Ookla speedtest server ID',
required: false,
schema: new OA\Schema(type: 'integer'),
description: 'Optional Ookla speedtest server ID'
schema: new OA\Schema(type: 'integer')
),
],
responses: [
new OA\Response(response: Response::HTTP_CREATED, description: 'Created'),
new OA\Response(response: Response::HTTP_FORBIDDEN, description: 'Forbidden'),
new OA\Response(response: Response::HTTP_UNPROCESSABLE_ENTITY, description: 'Validation error'),
new OA\Response(
response: Response::HTTP_CREATED,
description: 'Created',
content: new OA\JsonContent(ref: '#/components/schemas/SpeedtestRun')
),
new OA\Response(
response: Response::HTTP_UNAUTHORIZED,
description: 'Unauthenticated',
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
),
new OA\Response(
response: Response::HTTP_FORBIDDEN,
description: 'Forbidden',
content: new OA\JsonContent(ref: '#/components/schemas/ForbiddenError')
),
new OA\Response(
response: Response::HTTP_UNPROCESSABLE_ENTITY,
description: 'Validation error',
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
),
]
)]
public function __invoke(Request $request)
Expand Down
33 changes: 29 additions & 4 deletions app/Http/Controllers/Api/V1/ShowResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,36 @@ class ShowResult extends ApiController
{
#[OA\Get(
path: '/api/v1/results/{id}',
description: 'Get result.',
summary: 'Fetch a single result by ID',
operationId: 'getResult',
tags: ['Results'],
parameters: [
new OA\Parameter(
name: 'id',
in: 'path',
required: true,
schema: new OA\Schema(type: 'integer'),
description: 'The ID of the result'
),
],
responses: [
new OA\Response(response: 200, description: 'OK'),
new OA\Response(response: Response::HTTP_NOT_FOUND, description: 'Result not found'),
])]
new OA\Response(
response: Response::HTTP_OK,
description: 'OK',
content: new OA\JsonContent(ref: '#/components/schemas/ResultResponse')
),
new OA\Response(
response: Response::HTTP_UNAUTHORIZED,
description: 'Unauthenticated',
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
),
new OA\Response(
response: Response::HTTP_NOT_FOUND,
description: 'Result not found',
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundError')
),
]
)]
public function __invoke(Request $request, int $id)
{
$result = Result::findOr($id, function () {
Expand Down
42 changes: 42 additions & 0 deletions app/Http/Controllers/Api/V1/Stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
use App\Http\Resources\V1\StatResource;
use App\Models\Result;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use OpenApi\Attributes as OA;
use OpenApi\Attributes\Schema as OASchema;
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\Enums\FilterOperator;
use Spatie\QueryBuilder\QueryBuilder;
Expand All @@ -14,6 +17,45 @@ class Stats extends ApiController
/**
* Handle the incoming request.
*/
#[OA\Get(
path: '/api/v1/stats',
summary: 'Fetch aggregated Speedtest statistics',
operationId: 'getStats',
tags: ['Stats'],
parameters: [
new OA\Parameter(
name: 'start_at',
in: 'query',
description: 'ISO‑8601 start datetime filter',
required: false,
schema: new OASchema(type: 'string', format: 'date-time')
),
new OA\Parameter(
name: 'end_at',
in: 'query',
description: 'ISO‑8601 end datetime filter',
required: false,
schema: new OASchema(type: 'string', format: 'date-time')
),
],
responses: [
new OA\Response(
response: Response::HTTP_OK,
description: 'OK',
content: new OA\JsonContent(ref: '#/components/schemas/Stats')
),
new OA\Response(
response: Response::HTTP_UNAUTHORIZED,
description: 'Unauthenticated',
content: new OA\JsonContent(ref: '#/components/schemas/UnauthenticatedError')
),
new OA\Response(
response: Response::HTTP_UNPROCESSABLE_ENTITY,
description: 'Validation failed',
content: new OA\JsonContent(ref: '#/components/schemas/ValidationError')
),
]
)]
public function __invoke(Request $request)
{
$stats = QueryBuilder::for(Result::class)
Expand Down
38 changes: 38 additions & 0 deletions app/OpenApi/OpenApiDefinition.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\OpenApi;

use OpenApi\Attributes as OA;

#[OA\OpenApi(
info: new OA\Info(
title: 'Speedtest Tracker API',
version: '1.0.0',
),
components: new OA\Components(
securitySchemes: [
new OA\SecurityScheme(
securityScheme: 'bearerAuth',
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
),
],
parameters: [
new OA\Parameter(
name: 'Accept',
in: 'header',
required: true,
schema: new OA\Schema(type: 'string', default: 'application/json'),
description: 'Expected response format'
),
]
),
tags: [
new OA\Tag(name: 'Results', description: "Operations related to Speedtest results.\nRequires an API token with scope `results:read`."),
new OA\Tag(name: 'Speedtests', description: "Operations to run speedtests.\nRequires an API token with scope `speedtests:run`."),
new OA\Tag(name: 'Servers', description: "Operations for speedtest servers.\nRequires an API token with scope `ookla:list-servers`."),
new OA\Tag(name: 'Stats', description: "Operations for statistics.\nRequires an API token with scope `results:read`."),
]
)]
class OpenApiDefinition {}
19 changes: 19 additions & 0 deletions app/OpenApi/Schemas/ForbiddenErrorSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\OpenApi\Schemas;

use OpenApi\Attributes as OA;

#[OA\Schema(
schema: 'ForbiddenError',
type: 'object',
description: 'Forbidden error response when user lacks permission',
properties: [
new OA\Property(
property: 'message',
type: 'string',
description: 'Error message indicating lack of permission',
),
]
)]
class ForbiddenErrorSchema {}
19 changes: 19 additions & 0 deletions app/OpenApi/Schemas/NotFoundErrorSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace App\OpenApi\Schemas;

use OpenApi\Attributes as OA;

#[OA\Schema(
schema: 'NotFoundError',
type: 'object',
description: 'Error when a requested result is not found',
properties: [
new OA\Property(
property: 'message',
type: 'string',
description: 'Result not found error message',
),
],
)]
class NotFoundErrorSchema {}
23 changes: 23 additions & 0 deletions app/OpenApi/Schemas/ResultResponseSchema.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace App\OpenApi\Schemas;

use OpenApi\Attributes as OA;

#[OA\Schema(
schema: 'ResultResponse',
type: 'object',
description: 'Response for an Single Speedtest result entry',
properties: [
new OA\Property(
property: 'data',
ref: '#/components/schemas/Result'
),
new OA\Property(
property: 'message',
type: 'string',
description: 'Response status message',
),
]
)]
class ResultResponseSchema {}
Loading