From eb93904ad2441c3143ad47086183727aa3e2ae08 Mon Sep 17 00:00:00 2001 From: Phil Sturgeon <67381+philsturgeon@users.noreply.github.com> Date: Sun, 16 Nov 2025 11:35:43 +0000 Subject: [PATCH 1/5] wrong path readme --- frameworks-laravel/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frameworks-laravel/README.md b/frameworks-laravel/README.md index 57888ee..629437b 100644 --- a/frameworks-laravel/README.md +++ b/frameworks-laravel/README.md @@ -93,7 +93,7 @@ php artisan scribe:generate ## OpenAPI -The API description is exported by [Scribe](https://scribe.knuckles.wtf/laravel/) and can be found at `frameworks-laravel/storage/app/private/scribe/openapi.yaml`. +The API description is exported by [Scribe](https://scribe.knuckles.wtf/laravel/) and can be found in `./storage/app/private/scribe/openapi.yaml`. ## License From dded5b2b8e375add78f97427d4f474cb4d688af2 Mon Sep 17 00:00:00 2001 From: Phil Sturgeon <67381+philsturgeon@users.noreply.github.com> Date: Sun, 16 Nov 2025 15:13:34 +0000 Subject: [PATCH 2/5] added annotations --- .../.scribe/endpoints.cache/00.yaml | 140 +++++++- frameworks-laravel/.scribe/endpoints/00.yaml | 140 +++++++- .../Http/Controllers/Api/HealthController.php | 7 + .../Http/Controllers/Api/RaceController.php | 18 +- .../resources/views/scribe/index.blade.php | 325 +++++++++++++----- frameworks-laravel/routes/api.php | 7 +- 6 files changed, 533 insertions(+), 104 deletions(-) diff --git a/frameworks-laravel/.scribe/endpoints.cache/00.yaml b/frameworks-laravel/.scribe/endpoints.cache/00.yaml index 151bc32..6a1e02d 100644 --- a/frameworks-laravel/.scribe/endpoints.cache/00.yaml +++ b/frameworks-laravel/.scribe/endpoints.cache/00.yaml @@ -14,8 +14,11 @@ endpoints: groupDescription: '' subgroup: '' subgroupDescription: '' - title: '' - description: '' + title: Healthcheck + description: |- + Check that the service is up. If everything is okay, you'll get a 200 OK response. + + Otherwise, the request will fail with a 400 error, and a response listing the failed services. authenticated: false deprecated: false headers: @@ -32,7 +35,7 @@ endpoints: - custom: [] status: 200 - content: '{"status":"healthy","version":"unversioned","timestamp":"2025-11-03T10:55:55+00:00"}' + content: '{"status":"healthy","version":"unversioned","timestamp":"2025-11-16T15:08:22+00:00"}' headers: cache-control: 'no-cache, private' content-type: application/json @@ -238,17 +241,41 @@ endpoints: groupDescription: '' subgroup: '' subgroupDescription: '' - title: 'Display a listing of the resource.' - description: '' - authenticated: false + title: 'Get races' + description: 'A collection of race resources, newest first, optionally filtered by circuit or season query parameters.' + authenticated: true deprecated: false headers: Content-Type: application/json Accept: application/json urlParameters: [] cleanUrlParameters: [] - queryParameters: [] - cleanQueryParameters: [] + queryParameters: + season: + custom: [] + name: season + description: 'Filter the results by season year' + required: false + example: '2024' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + circuit: + custom: [] + name: circuit + description: 'Filter the results by circuit name' + required: false + example: Monaco + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanQueryParameters: + season: '2024' + circuit: Monaco bodyParameters: [] cleanBodyParameters: [] fileParameters: [] @@ -256,7 +283,7 @@ endpoints: - custom: [] status: 200 - content: '{"data":[{"id":1,"name":"2024 Monaco Grand Prix","race_date":"2024-05-26","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/1","circuit":"http:\/\/localhost\/api\/circuits\/1","drivers":"http:\/\/localhost\/api\/drivers?race=1"}},{"id":2,"name":"2024 British Grand Prix","race_date":"2024-07-07","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/2","circuit":"http:\/\/localhost\/api\/circuits\/2","drivers":"http:\/\/localhost\/api\/drivers?race=2"}},{"id":3,"name":"2024 Italian Grand Prix","race_date":"2024-09-01","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/3","circuit":"http:\/\/localhost\/api\/circuits\/3","drivers":"http:\/\/localhost\/api\/drivers?race=3"}},{"id":4,"name":"2024 Belgian Grand Prix","race_date":"2024-07-28","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/4","circuit":"http:\/\/localhost\/api\/circuits\/4","drivers":"http:\/\/localhost\/api\/drivers?race=4"}},{"id":5,"name":"2024 Japanese Grand Prix","race_date":"2024-04-07","season":"2024","created_at":"2025-10-29T17:21:40+00:00","updated_at":"2025-10-29T17:21:40+00:00","links":{"self":"http:\/\/localhost\/api\/races\/5","circuit":"http:\/\/localhost\/api\/circuits\/5","drivers":"http:\/\/localhost\/api\/drivers?race=5"}}],"meta":{"count":5}}' + content: '{"data":[],"meta":{"count":0}}' headers: cache-control: 'no-cache, private' content-type: application/json @@ -319,3 +346,98 @@ endpoints: controller: null method: null route: null + - + custom: [] + httpMethods: + - POST + uri: api/races + metadata: + custom: [] + groupName: Endpoints + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Create a race' + description: 'Allows authenticated users to submit a new Race resource to the system.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: [] + cleanUrlParameters: [] + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + name: + custom: [] + name: name + description: 'The name of the race.' + required: true + example: 'Monaco Grand Prix' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + circuit_id: + custom: [] + name: circuit_id + description: 'The Unique Identifier for the circuit where the race will be held.' + required: true + example: 1234-1234-1234-1234 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + race_date: + custom: [] + name: race_date + description: 'The date and time the race takes place, RFC 3339 in local timezone.' + required: true + example: '2024-05-26T14:53:59' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + season: + custom: [] + name: season + description: 'The season year for this race.' + required: true + example: '2024' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + driver_ids: + custom: [] + name: driver_ids + description: 'An array of Unique Identifiers for drivers participating in the race.' + required: false + example: + - 5678-5678-5678-5678 + - 6789-6789-6789-6789 + type: 'string[]' + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanBodyParameters: + name: 'Monaco Grand Prix' + circuit_id: 1234-1234-1234-1234 + race_date: '2024-05-26T14:53:59' + season: '2024' + driver_ids: + - 5678-5678-5678-5678 + - 6789-6789-6789-6789 + fileParameters: [] + responses: [] + responseFields: [] + auth: [] + controller: null + method: null + route: null diff --git a/frameworks-laravel/.scribe/endpoints/00.yaml b/frameworks-laravel/.scribe/endpoints/00.yaml index b7775ac..6166bee 100644 --- a/frameworks-laravel/.scribe/endpoints/00.yaml +++ b/frameworks-laravel/.scribe/endpoints/00.yaml @@ -12,8 +12,11 @@ endpoints: groupDescription: '' subgroup: '' subgroupDescription: '' - title: '' - description: '' + title: Healthcheck + description: |- + Check that the service is up. If everything is okay, you'll get a 200 OK response. + + Otherwise, the request will fail with a 400 error, and a response listing the failed services. authenticated: false deprecated: false headers: @@ -30,7 +33,7 @@ endpoints: - custom: [] status: 200 - content: '{"status":"healthy","version":"unversioned","timestamp":"2025-11-03T10:55:55+00:00"}' + content: '{"status":"healthy","version":"unversioned","timestamp":"2025-11-16T15:08:22+00:00"}' headers: cache-control: 'no-cache, private' content-type: application/json @@ -236,17 +239,41 @@ endpoints: groupDescription: '' subgroup: '' subgroupDescription: '' - title: 'Display a listing of the resource.' - description: '' - authenticated: false + title: 'Get races' + description: 'A collection of race resources, newest first, optionally filtered by circuit or season query parameters.' + authenticated: true deprecated: false headers: Content-Type: application/json Accept: application/json urlParameters: [] cleanUrlParameters: [] - queryParameters: [] - cleanQueryParameters: [] + queryParameters: + season: + custom: [] + name: season + description: 'Filter the results by season year' + required: false + example: '2024' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + circuit: + custom: [] + name: circuit + description: 'Filter the results by circuit name' + required: false + example: Monaco + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanQueryParameters: + season: '2024' + circuit: Monaco bodyParameters: [] cleanBodyParameters: [] fileParameters: [] @@ -254,7 +281,7 @@ endpoints: - custom: [] status: 200 - content: '{"data":[{"id":1,"name":"2024 Monaco Grand Prix","race_date":"2024-05-26","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/1","circuit":"http:\/\/localhost\/api\/circuits\/1","drivers":"http:\/\/localhost\/api\/drivers?race=1"}},{"id":2,"name":"2024 British Grand Prix","race_date":"2024-07-07","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/2","circuit":"http:\/\/localhost\/api\/circuits\/2","drivers":"http:\/\/localhost\/api\/drivers?race=2"}},{"id":3,"name":"2024 Italian Grand Prix","race_date":"2024-09-01","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/3","circuit":"http:\/\/localhost\/api\/circuits\/3","drivers":"http:\/\/localhost\/api\/drivers?race=3"}},{"id":4,"name":"2024 Belgian Grand Prix","race_date":"2024-07-28","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/4","circuit":"http:\/\/localhost\/api\/circuits\/4","drivers":"http:\/\/localhost\/api\/drivers?race=4"}},{"id":5,"name":"2024 Japanese Grand Prix","race_date":"2024-04-07","season":"2024","created_at":"2025-10-29T17:21:40+00:00","updated_at":"2025-10-29T17:21:40+00:00","links":{"self":"http:\/\/localhost\/api\/races\/5","circuit":"http:\/\/localhost\/api\/circuits\/5","drivers":"http:\/\/localhost\/api\/drivers?race=5"}}],"meta":{"count":5}}' + content: '{"data":[],"meta":{"count":0}}' headers: cache-control: 'no-cache, private' content-type: application/json @@ -317,3 +344,98 @@ endpoints: controller: null method: null route: null + - + custom: [] + httpMethods: + - POST + uri: api/races + metadata: + custom: [] + groupName: Endpoints + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Create a race' + description: 'Allows authenticated users to submit a new Race resource to the system.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: [] + cleanUrlParameters: [] + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + name: + custom: [] + name: name + description: 'The name of the race.' + required: true + example: 'Monaco Grand Prix' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + circuit_id: + custom: [] + name: circuit_id + description: 'The Unique Identifier for the circuit where the race will be held.' + required: true + example: 1234-1234-1234-1234 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + race_date: + custom: [] + name: race_date + description: 'The date and time the race takes place, RFC 3339 in local timezone.' + required: true + example: '2024-05-26T14:53:59' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + season: + custom: [] + name: season + description: 'The season year for this race.' + required: true + example: '2024' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + driver_ids: + custom: [] + name: driver_ids + description: 'An array of Unique Identifiers for drivers participating in the race.' + required: false + example: + - 5678-5678-5678-5678 + - 6789-6789-6789-6789 + type: 'string[]' + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanBodyParameters: + name: 'Monaco Grand Prix' + circuit_id: 1234-1234-1234-1234 + race_date: '2024-05-26T14:53:59' + season: '2024' + driver_ids: + - 5678-5678-5678-5678 + - 6789-6789-6789-6789 + fileParameters: [] + responses: [] + responseFields: [] + auth: [] + controller: null + method: null + route: null diff --git a/frameworks-laravel/app/Http/Controllers/Api/HealthController.php b/frameworks-laravel/app/Http/Controllers/Api/HealthController.php index 7e55f26..9559e6e 100644 --- a/frameworks-laravel/app/Http/Controllers/Api/HealthController.php +++ b/frameworks-laravel/app/Http/Controllers/Api/HealthController.php @@ -7,6 +7,13 @@ class HealthController extends Controller { + /** + * Healthcheck + * + * Check that the service is up. If everything is okay, you'll get a 200 OK response. + * + * Otherwise, the request will fail with a 400 error, and a response listing the failed services. + */ public function show(): JsonResponse { return response()->json([ diff --git a/frameworks-laravel/app/Http/Controllers/Api/RaceController.php b/frameworks-laravel/app/Http/Controllers/Api/RaceController.php index 759c690..985ccb0 100644 --- a/frameworks-laravel/app/Http/Controllers/Api/RaceController.php +++ b/frameworks-laravel/app/Http/Controllers/Api/RaceController.php @@ -7,12 +7,18 @@ use App\Http\Resources\RaceCollection; use App\Models\Race; use Illuminate\Http\Request; +use Knuckles\Scribe\Attributes\{Authenticated, BodyParam, QueryParam}; class RaceController extends Controller { /** - * Display a listing of the resource. + * Get races + * + * A collection of race resources, newest first, optionally filtered by circuit or season query parameters. */ + #[Authenticated] + #[QueryParam(name: 'season', type: 'string', description: 'Filter the results by season year', required: false, example: '2024')] + #[QueryParam(name: 'circuit', type: 'string', description: 'Filter the results by circuit name', required: false, example: 'Monaco')] public function index(Request $request): RaceCollection { $query = Race::query(); @@ -29,8 +35,16 @@ public function index(Request $request): RaceCollection } /** - * Store a newly created resource in storage. + * Create a race + * + * Allows authenticated users to submit a new Race resource to the system. */ + #[Authenticated] + #[BodyParam(name: 'name', type: 'string', description: 'The name of the race.', required: true, example: 'Monaco Grand Prix')] + #[BodyParam(name: 'race_date', type: 'string', description: 'The date and time the race takes place, RFC 3339 in local timezone.', required: true, example: '2024-05-26T14:53:59')] + #[BodyParam(name: 'circuit_id', type: 'string', description: 'The Unique Identifier for the circuit where the race will be held.', required: true, example: '1234-1234-1234-1234')] + #[BodyParam(name: 'season', type: 'string', description: 'The season year for this race.', required: true, example: '2024')] + #[BodyParam(name: 'driver_ids', type: 'array', description: 'An array of Unique Identifiers for drivers participating in the race.', required: false, example: [ "5678-5678-5678-5678", "6789-6789-6789-6789" ])] public function store(Request $request): RaceResource { $validated = $request->validate([ diff --git a/frameworks-laravel/resources/views/scribe/index.blade.php b/frameworks-laravel/resources/views/scribe/index.blade.php index 7d47a05..6240ef5 100644 --- a/frameworks-laravel/resources/views/scribe/index.blade.php +++ b/frameworks-laravel/resources/views/scribe/index.blade.php @@ -72,7 +72,7 @@
- +
Check that the service is up. If everything is okay, you'll get a 200 OK response.
+Otherwise, the request will fail with a 400 error, and a response listing the failed services.
Example request:@@ -176,7 +180,7 @@
{
"status": "healthy",
"version": "unversioned",
- "timestamp": "2025-11-03T10:55:55+00:00"
+ "timestamp": "2025-11-16T15:08:22+00:00"
}
@@ -943,12 +947,13 @@
- +requires authentication
- +A collection of race resources, newest first, optionally filtered by circuit or season query parameters.
Example request:@@ -956,7 +961,7 @@
curl --request GET \
- --get "http://localhost/api/races" \
+ --get "http://localhost/api/races?season=2024&circuit=Monaco" \
--header "Content-Type: application/json" \
--header "Accept: application/json"
{
- "data": [
- {
- "id": 1,
- "name": "2024 Monaco Grand Prix",
- "race_date": "2024-05-26",
- "season": "2024",
- "created_at": "2025-10-29T17:21:39+00:00",
- "updated_at": "2025-10-29T17:21:39+00:00",
- "links": {
- "self": "http://localhost/api/races/1",
- "circuit": "http://localhost/api/circuits/1",
- "drivers": "http://localhost/api/drivers?race=1"
- }
- },
- {
- "id": 2,
- "name": "2024 British Grand Prix",
- "race_date": "2024-07-07",
- "season": "2024",
- "created_at": "2025-10-29T17:21:39+00:00",
- "updated_at": "2025-10-29T17:21:39+00:00",
- "links": {
- "self": "http://localhost/api/races/2",
- "circuit": "http://localhost/api/circuits/2",
- "drivers": "http://localhost/api/drivers?race=2"
- }
- },
- {
- "id": 3,
- "name": "2024 Italian Grand Prix",
- "race_date": "2024-09-01",
- "season": "2024",
- "created_at": "2025-10-29T17:21:39+00:00",
- "updated_at": "2025-10-29T17:21:39+00:00",
- "links": {
- "self": "http://localhost/api/races/3",
- "circuit": "http://localhost/api/circuits/3",
- "drivers": "http://localhost/api/drivers?race=3"
- }
- },
- {
- "id": 4,
- "name": "2024 Belgian Grand Prix",
- "race_date": "2024-07-28",
- "season": "2024",
- "created_at": "2025-10-29T17:21:39+00:00",
- "updated_at": "2025-10-29T17:21:39+00:00",
- "links": {
- "self": "http://localhost/api/races/4",
- "circuit": "http://localhost/api/circuits/4",
- "drivers": "http://localhost/api/drivers?race=4"
- }
- },
- {
- "id": 5,
- "name": "2024 Japanese Grand Prix",
- "race_date": "2024-04-07",
- "season": "2024",
- "created_at": "2025-10-29T17:21:40+00:00",
- "updated_at": "2025-10-29T17:21:40+00:00",
- "links": {
- "self": "http://localhost/api/races/5",
- "circuit": "http://localhost/api/circuits/5",
- "drivers": "http://localhost/api/drivers?race=5"
- }
- }
- ],
+ "data": [],
"meta": {
- "count": 5
+ "count": 0
}
}
@@ -1082,7 +1028,7 @@
+ season
+string
+optional
+
+
+ Filter the results by season year Example: 2024
circuit
+string
+optional
+
+
+ Filter the results by circuit name Example: Monaco
+
+ +Allows authenticated users to submit a new Race resource to the system.
+ + +Example request:+ + +
curl --request POST \
+ "http://localhost/api/races" \
+ --header "Content-Type: application/json" \
+ --header "Accept: application/json" \
+ --data "{
+ \"name\": \"Monaco Grand Prix\",
+ \"circuit_id\": \"1234-1234-1234-1234\",
+ \"race_date\": \"2024-05-26T14:53:59\",
+ \"season\": \"2024\",
+ \"driver_ids\": [
+ \"5678-5678-5678-5678\",
+ \"6789-6789-6789-6789\"
+ ]
+}"
+const url = new URL(
+ "http://localhost/api/races"
+);
+
+const headers = {
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+};
+
+let body = {
+ "name": "Monaco Grand Prix",
+ "circuit_id": "1234-1234-1234-1234",
+ "race_date": "2024-05-26T14:53:59",
+ "season": "2024",
+ "driver_ids": [
+ "5678-5678-5678-5678",
+ "6789-6789-6789-6789"
+ ]
+};
+
+fetch(url, {
+ method: "POST",
+ headers,
+ body: JSON.stringify(body),
+}).then(response => response.json());Received response: ++
+
+
+ Request failed with error:+
+
+Tip: Check that you're properly connected to the network.
+If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
+You can check the Dev Tools console for debugging information.
+
+
+
diff --git a/frameworks-laravel/routes/api.php b/frameworks-laravel/routes/api.php
index 6738487..2958424 100644
--- a/frameworks-laravel/routes/api.php
+++ b/frameworks-laravel/routes/api.php
@@ -4,25 +4,22 @@
use App\Http\Controllers\Api\DriverController;
use App\Http\Controllers\Api\HealthController;
use App\Http\Controllers\Api\RaceController;
-use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
// API routes (no version prefix, base /api is automatic)
+
// Health check
Route::get('/health', [HealthController::class, 'show']);
// Drivers
Route::get('/drivers', [DriverController::class, 'index']);
Route::get('/drivers/{id}', [DriverController::class, 'show']);
-// GET-only demo: removed create/update/delete routes
// Circuits
Route::get('/circuits', [CircuitController::class, 'index']);
Route::get('/circuits/{id}', [CircuitController::class, 'show']);
-// GET-only demo: removed create/update/delete routes
// Races
Route::get('/races', [RaceController::class, 'index']);
Route::get('/races/{id}', [RaceController::class, 'show']);
-// GET-only demo: removed create/update/delete routes
-// Removed granular driver attach/detach endpoints to simplify API
+Route::post('/races', [RaceController::class, 'store']);
From 87bd9e01d4d5cef47313ceb4c26c7824397bb749 Mon Sep 17 00:00:00 2001
From: Phil Sturgeon <67381+philsturgeon@users.noreply.github.com>
Date: Sun, 16 Nov 2025 15:26:06 +0000
Subject: [PATCH 3/5] added laravel tests
---
.../tests/Feature/CircuitsTest.php | 66 +++++++++++++++++++
.../tests/Feature/DriversTest.php | 66 +++++++++++++++++++
.../tests/Feature/HealthTest.php | 19 ++++++
.../tests/Feature/RacesTest.php | 59 +++++++++++++++++
frameworks-laravel/tests/Pest.php | 3 +
5 files changed, 213 insertions(+)
create mode 100644 frameworks-laravel/tests/Feature/CircuitsTest.php
create mode 100644 frameworks-laravel/tests/Feature/DriversTest.php
create mode 100644 frameworks-laravel/tests/Feature/HealthTest.php
create mode 100644 frameworks-laravel/tests/Feature/RacesTest.php
create mode 100644 frameworks-laravel/tests/Pest.php
diff --git a/frameworks-laravel/tests/Feature/CircuitsTest.php b/frameworks-laravel/tests/Feature/CircuitsTest.php
new file mode 100644
index 0000000..ec5c9cb
--- /dev/null
+++ b/frameworks-laravel/tests/Feature/CircuitsTest.php
@@ -0,0 +1,66 @@
+seed();
+ });
+
+ test('returns list of all circuits', function () {
+ $response = $this->getJson('/api/circuits');
+
+ $response->assertStatus(200)
+ ->assertJsonStructure(['data', 'meta']);
+
+ $data = $response->json('data');
+ expect($data)->toBeArray()->not->toBeEmpty();
+ });
+
+ test('returns correct data structure', function () {
+ $response = $this->getJson('/api/circuits');
+
+ $data = $response->json('data');
+ expect($data)->toBeArray();
+
+ if (count($data) > 0) {
+ $circuit = $data[0];
+ expect($circuit)->toHaveKeys(['id', 'name', 'location']);
+ }
+ });
+});
+
+describe('GET /api/circuits/{id}', function () {
+
+ beforeEach(function () {
+ $this->seed();
+ });
+
+ test('returns a specific circuit by id', function () {
+ $response = $this->getJson('/api/circuits');
+ $circuits = $response->json('data');
+ expect($circuits)->toBeArray()->not->toBeEmpty();
+
+ $firstCircuit = $circuits[0];
+
+ $response = $this->getJson("/api/circuits/{$firstCircuit['id']}");
+
+ $response->assertStatus(200)
+ ->assertJsonStructure(['data' => ['id', 'name', 'location']])
+ ->assertJson([
+ 'data' => [
+ 'id' => $firstCircuit['id'],
+ 'name' => $firstCircuit['name'],
+ 'location' => $firstCircuit['location'],
+ ],
+ ]);
+ });
+
+ test('returns 404 for non-existent circuit', function () {
+ $response = $this->getJson('/api/circuits/99999');
+ $response->assertStatus(404);
+ });
+});
diff --git a/frameworks-laravel/tests/Feature/DriversTest.php b/frameworks-laravel/tests/Feature/DriversTest.php
new file mode 100644
index 0000000..41939a9
--- /dev/null
+++ b/frameworks-laravel/tests/Feature/DriversTest.php
@@ -0,0 +1,66 @@
+seed();
+ });
+
+ test('returns list of all drivers', function () {
+ $response = $this->getJson('/api/drivers');
+
+ $response->assertStatus(200)
+ ->assertJsonStructure(['data', 'meta']);
+
+ $data = $response->json('data');
+ expect($data)->toBeArray()->not->toBeEmpty();
+ });
+
+ test('returns correct data structure', function () {
+ $response = $this->getJson('/api/drivers');
+
+ $data = $response->json('data');
+ expect($data)->toBeArray();
+
+ if (count($data) > 0) {
+ $driver = $data[0];
+ expect($driver)->toHaveKeys(['id', 'name', 'code']);
+ }
+ });
+});
+
+describe('GET /api/drivers/{id}', function () {
+
+ beforeEach(function () {
+ $this->seed();
+ });
+
+ test('returns a specific driver by id', function () {
+ $response = $this->getJson('/api/drivers');
+ $drivers = $response->json('data');
+ expect($drivers)->toBeArray()->not->toBeEmpty();
+
+ $firstDriver = $drivers[0];
+
+ $response = $this->getJson("/api/drivers/{$firstDriver['id']}");
+
+ $response->assertStatus(200)
+ ->assertJsonStructure(['data' => ['id', 'name', 'code']])
+ ->assertJson([
+ 'data' => [
+ 'id' => $firstDriver['id'],
+ 'name' => $firstDriver['name'],
+ 'code' => $firstDriver['code'],
+ ],
+ ]);
+ });
+
+ test('returns 404 for non-existent driver', function () {
+ $response = $this->getJson('/api/drivers/99999');
+ $response->assertStatus(404);
+ });
+});
diff --git a/frameworks-laravel/tests/Feature/HealthTest.php b/frameworks-laravel/tests/Feature/HealthTest.php
new file mode 100644
index 0000000..9634638
--- /dev/null
+++ b/frameworks-laravel/tests/Feature/HealthTest.php
@@ -0,0 +1,19 @@
+seed();
+ });
+
+ test('returns success status', function () {
+ $response = $this->getJson('/api/health');
+
+ $response->assertStatus(200)
+ ->assertJsonStructure(['status']);
+ });
+});
diff --git a/frameworks-laravel/tests/Feature/RacesTest.php b/frameworks-laravel/tests/Feature/RacesTest.php
new file mode 100644
index 0000000..27b2b6d
--- /dev/null
+++ b/frameworks-laravel/tests/Feature/RacesTest.php
@@ -0,0 +1,59 @@
+seed();
+ });
+
+ test('returns list of all races', function () {
+ $response = $this->getJson('/api/races');
+
+ $response->assertStatus(200)
+ ->assertJsonStructure(['data', 'meta']);
+
+ $data = $response->json('data');
+ expect($data)->toBeArray()->not->toBeEmpty();
+ });
+
+ test('returns correct data structure', function () {
+ $response = $this->getJson('/api/races');
+
+ $data = $response->json('data');
+ expect($data)->toBeArray();
+
+ if (count($data) > 0) {
+ $race = $data[0];
+ expect($race)->toHaveKeys(['id', 'name', 'season']);
+ }
+ });
+});
+
+describe('GET /api/races/{id}', function () {
+
+ beforeEach(function () {
+ $this->seed();
+ });
+
+ test('returns a specific race by id', function () {
+ $response = $this->getJson('/api/races');
+ $races = $response->json('data');
+ expect($races)->toBeArray()->not->toBeEmpty();
+
+ $firstRace = $races[0];
+
+ $response = $this->getJson("/api/races/{$firstRace['id']}");
+
+ $response->assertStatus(200)
+ ->assertJsonStructure(['data' => ['id', 'name', 'season', 'race_date', 'links']]);
+ });
+
+ test('returns 404 for non-existent race', function () {
+ $response = $this->getJson('/api/races/99999');
+ $response->assertStatus(404);
+ });
+});
diff --git a/frameworks-laravel/tests/Pest.php b/frameworks-laravel/tests/Pest.php
new file mode 100644
index 0000000..a7b28ca
--- /dev/null
+++ b/frameworks-laravel/tests/Pest.php
@@ -0,0 +1,3 @@
+in('Feature');
From e669eab87eca65af52d392553aa8e89f1bfbc674 Mon Sep 17 00:00:00 2001
From: Phil Sturgeon <67381+philsturgeon@users.noreply.github.com>
Date: Sun, 16 Nov 2025 15:28:46 +0000
Subject: [PATCH 4/5] git ignore .scribe
---
frameworks-laravel/.gitignore | 4 +-
frameworks-laravel/.scribe/.filehashes | 4 -
frameworks-laravel/.scribe/auth.md | 3 -
.../.scribe/endpoints.cache/00.yaml | 443 ------------------
frameworks-laravel/.scribe/endpoints/00.yaml | 441 -----------------
.../.scribe/endpoints/custom.0.yaml | 53 ---
frameworks-laravel/.scribe/intro.md | 11 -
7 files changed, 3 insertions(+), 956 deletions(-)
delete mode 100644 frameworks-laravel/.scribe/.filehashes
delete mode 100644 frameworks-laravel/.scribe/auth.md
delete mode 100644 frameworks-laravel/.scribe/endpoints.cache/00.yaml
delete mode 100644 frameworks-laravel/.scribe/endpoints/00.yaml
delete mode 100644 frameworks-laravel/.scribe/endpoints/custom.0.yaml
delete mode 100644 frameworks-laravel/.scribe/intro.md
diff --git a/frameworks-laravel/.gitignore b/frameworks-laravel/.gitignore
index 3113dfe..95c2342 100644
--- a/frameworks-laravel/.gitignore
+++ b/frameworks-laravel/.gitignore
@@ -8,7 +8,6 @@
/.fleet
/.idea
/.nova
-/.scribe
/.phpunit.cache
/.vscode
/.zed
@@ -23,3 +22,6 @@
Homestead.json
Homestead.yaml
Thumbs.db
+
+# Scribe
+.scribe
diff --git a/frameworks-laravel/.scribe/.filehashes b/frameworks-laravel/.scribe/.filehashes
deleted file mode 100644
index 51da126..0000000
--- a/frameworks-laravel/.scribe/.filehashes
+++ /dev/null
@@ -1,4 +0,0 @@
-# GENERATED. YOU SHOULDN'T MODIFY OR DELETE THIS FILE.
-# Scribe uses this file to know when you change something manually in your docs.
-.scribe/intro.md=35310953ddf7f9e68771f7ed7b6189f9
-.scribe/auth.md=9bee2b1ef8a238b2e58613fa636d5f39
\ No newline at end of file
diff --git a/frameworks-laravel/.scribe/auth.md b/frameworks-laravel/.scribe/auth.md
deleted file mode 100644
index 8290362..0000000
--- a/frameworks-laravel/.scribe/auth.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Authenticating requests
-
-This API is not authenticated.
diff --git a/frameworks-laravel/.scribe/endpoints.cache/00.yaml b/frameworks-laravel/.scribe/endpoints.cache/00.yaml
deleted file mode 100644
index 6a1e02d..0000000
--- a/frameworks-laravel/.scribe/endpoints.cache/00.yaml
+++ /dev/null
@@ -1,443 +0,0 @@
-## Autogenerated by Scribe. DO NOT MODIFY.
-
-name: Endpoints
-description: ''
-endpoints:
- -
- custom: []
- httpMethods:
- - GET
- uri: api/health
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: Healthcheck
- description: |-
- Check that the service is up. If everything is okay, you'll get a 200 OK response.
-
- Otherwise, the request will fail with a 400 error, and a response listing the failed services.
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"status":"healthy","version":"unversioned","timestamp":"2025-11-16T15:08:22+00:00"}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: api/drivers
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display a listing of the resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":[{"id":1,"name":"Max Verstappen","code":"VER","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":2,"name":"Sergio Perez","code":"PER","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":3,"name":"Lewis Hamilton","code":"HAM","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":4,"name":"George Russell","code":"RUS","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":5,"name":"Charles Leclerc","code":"LEC","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":6,"name":"Carlos Sainz","code":"SAI","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":7,"name":"Lando Norris","code":"NOR","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":8,"name":"Oscar Piastri","code":"PIA","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":9,"name":"Fernando Alonso","code":"ALO","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":10,"name":"Lance Stroll","code":"STR","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"}],"meta":{"count":10}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: 'api/drivers/{id}'
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display the specified resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters:
- id:
- custom: []
- name: id
- description: 'The ID of the driver.'
- required: true
- example: 1
- type: integer
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanUrlParameters:
- id: 1
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":{"id":1,"name":"Max Verstappen","code":"VER","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: api/circuits
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display a listing of the resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":[{"id":1,"name":"Monaco Grand Prix","location":"Monte Carlo, Monaco","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":2,"name":"British Grand Prix","location":"Silverstone, United Kingdom","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":3,"name":"Italian Grand Prix","location":"Monza, Italy","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":4,"name":"Belgian Grand Prix","location":"Spa-Francorchamps, Belgium","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":5,"name":"Japanese Grand Prix","location":"Suzuka, Japan","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":6,"name":"Singapore Grand Prix","location":"Marina Bay, Singapore","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":7,"name":"Abu Dhabi Grand Prix","location":"Yas Marina, UAE","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":8,"name":"Brazilian Grand Prix","location":"Interlagos, Brazil","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":9,"name":"Australian Grand Prix","location":"Melbourne, Australia","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":10,"name":"Spanish Grand Prix","location":"Barcelona, Spain","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"}],"meta":{"count":10}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: 'api/circuits/{id}'
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display the specified resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters:
- id:
- custom: []
- name: id
- description: 'The ID of the circuit.'
- required: true
- example: 1
- type: integer
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanUrlParameters:
- id: 1
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":{"id":1,"name":"Monaco Grand Prix","location":"Monte Carlo, Monaco","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: api/races
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Get races'
- description: 'A collection of race resources, newest first, optionally filtered by circuit or season query parameters.'
- authenticated: true
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters:
- season:
- custom: []
- name: season
- description: 'Filter the results by season year'
- required: false
- example: '2024'
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- circuit:
- custom: []
- name: circuit
- description: 'Filter the results by circuit name'
- required: false
- example: Monaco
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanQueryParameters:
- season: '2024'
- circuit: Monaco
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":[],"meta":{"count":0}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: 'api/races/{id}'
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display the specified resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters:
- id:
- custom: []
- name: id
- description: 'The ID of the race.'
- required: true
- example: 1
- type: integer
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanUrlParameters:
- id: 1
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":{"id":1,"name":"2024 Monaco Grand Prix","race_date":"2024-05-26","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/1","circuit":"http:\/\/localhost\/api\/circuits\/1","drivers":"http:\/\/localhost\/api\/drivers?race=1"}}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - POST
- uri: api/races
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Create a race'
- description: 'Allows authenticated users to submit a new Race resource to the system.'
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters:
- name:
- custom: []
- name: name
- description: 'The name of the race.'
- required: true
- example: 'Monaco Grand Prix'
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- circuit_id:
- custom: []
- name: circuit_id
- description: 'The Unique Identifier for the circuit where the race will be held.'
- required: true
- example: 1234-1234-1234-1234
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- race_date:
- custom: []
- name: race_date
- description: 'The date and time the race takes place, RFC 3339 in local timezone.'
- required: true
- example: '2024-05-26T14:53:59'
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- season:
- custom: []
- name: season
- description: 'The season year for this race.'
- required: true
- example: '2024'
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- driver_ids:
- custom: []
- name: driver_ids
- description: 'An array of Unique Identifiers for drivers participating in the race.'
- required: false
- example:
- - 5678-5678-5678-5678
- - 6789-6789-6789-6789
- type: 'string[]'
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanBodyParameters:
- name: 'Monaco Grand Prix'
- circuit_id: 1234-1234-1234-1234
- race_date: '2024-05-26T14:53:59'
- season: '2024'
- driver_ids:
- - 5678-5678-5678-5678
- - 6789-6789-6789-6789
- fileParameters: []
- responses: []
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
diff --git a/frameworks-laravel/.scribe/endpoints/00.yaml b/frameworks-laravel/.scribe/endpoints/00.yaml
deleted file mode 100644
index 6166bee..0000000
--- a/frameworks-laravel/.scribe/endpoints/00.yaml
+++ /dev/null
@@ -1,441 +0,0 @@
-name: Endpoints
-description: ''
-endpoints:
- -
- custom: []
- httpMethods:
- - GET
- uri: api/health
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: Healthcheck
- description: |-
- Check that the service is up. If everything is okay, you'll get a 200 OK response.
-
- Otherwise, the request will fail with a 400 error, and a response listing the failed services.
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"status":"healthy","version":"unversioned","timestamp":"2025-11-16T15:08:22+00:00"}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: api/drivers
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display a listing of the resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":[{"id":1,"name":"Max Verstappen","code":"VER","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":2,"name":"Sergio Perez","code":"PER","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":3,"name":"Lewis Hamilton","code":"HAM","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":4,"name":"George Russell","code":"RUS","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":5,"name":"Charles Leclerc","code":"LEC","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":6,"name":"Carlos Sainz","code":"SAI","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":7,"name":"Lando Norris","code":"NOR","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":8,"name":"Oscar Piastri","code":"PIA","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":9,"name":"Fernando Alonso","code":"ALO","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":10,"name":"Lance Stroll","code":"STR","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"}],"meta":{"count":10}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: 'api/drivers/{id}'
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display the specified resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters:
- id:
- custom: []
- name: id
- description: 'The ID of the driver.'
- required: true
- example: 1
- type: integer
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanUrlParameters:
- id: 1
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":{"id":1,"name":"Max Verstappen","code":"VER","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: api/circuits
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display a listing of the resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":[{"id":1,"name":"Monaco Grand Prix","location":"Monte Carlo, Monaco","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":2,"name":"British Grand Prix","location":"Silverstone, United Kingdom","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":3,"name":"Italian Grand Prix","location":"Monza, Italy","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":4,"name":"Belgian Grand Prix","location":"Spa-Francorchamps, Belgium","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":5,"name":"Japanese Grand Prix","location":"Suzuka, Japan","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":6,"name":"Singapore Grand Prix","location":"Marina Bay, Singapore","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":7,"name":"Abu Dhabi Grand Prix","location":"Yas Marina, UAE","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":8,"name":"Brazilian Grand Prix","location":"Interlagos, Brazil","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":9,"name":"Australian Grand Prix","location":"Melbourne, Australia","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"},{"id":10,"name":"Spanish Grand Prix","location":"Barcelona, Spain","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"}],"meta":{"count":10}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: 'api/circuits/{id}'
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display the specified resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters:
- id:
- custom: []
- name: id
- description: 'The ID of the circuit.'
- required: true
- example: 1
- type: integer
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanUrlParameters:
- id: 1
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":{"id":1,"name":"Monaco Grand Prix","location":"Monte Carlo, Monaco","created_at":"2025-10-29T17:21:39.000000Z","updated_at":"2025-10-29T17:21:39.000000Z"}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: api/races
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Get races'
- description: 'A collection of race resources, newest first, optionally filtered by circuit or season query parameters.'
- authenticated: true
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters:
- season:
- custom: []
- name: season
- description: 'Filter the results by season year'
- required: false
- example: '2024'
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- circuit:
- custom: []
- name: circuit
- description: 'Filter the results by circuit name'
- required: false
- example: Monaco
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanQueryParameters:
- season: '2024'
- circuit: Monaco
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":[],"meta":{"count":0}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - GET
- uri: 'api/races/{id}'
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Display the specified resource.'
- description: ''
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters:
- id:
- custom: []
- name: id
- description: 'The ID of the race.'
- required: true
- example: 1
- type: integer
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanUrlParameters:
- id: 1
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters: []
- cleanBodyParameters: []
- fileParameters: []
- responses:
- -
- custom: []
- status: 200
- content: '{"data":{"id":1,"name":"2024 Monaco Grand Prix","race_date":"2024-05-26","season":"2024","created_at":"2025-10-29T17:21:39+00:00","updated_at":"2025-10-29T17:21:39+00:00","links":{"self":"http:\/\/localhost\/api\/races\/1","circuit":"http:\/\/localhost\/api\/circuits\/1","drivers":"http:\/\/localhost\/api\/drivers?race=1"}}}'
- headers:
- cache-control: 'no-cache, private'
- content-type: application/json
- access-control-allow-origin: '*'
- description: null
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
- -
- custom: []
- httpMethods:
- - POST
- uri: api/races
- metadata:
- custom: []
- groupName: Endpoints
- groupDescription: ''
- subgroup: ''
- subgroupDescription: ''
- title: 'Create a race'
- description: 'Allows authenticated users to submit a new Race resource to the system.'
- authenticated: false
- deprecated: false
- headers:
- Content-Type: application/json
- Accept: application/json
- urlParameters: []
- cleanUrlParameters: []
- queryParameters: []
- cleanQueryParameters: []
- bodyParameters:
- name:
- custom: []
- name: name
- description: 'The name of the race.'
- required: true
- example: 'Monaco Grand Prix'
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- circuit_id:
- custom: []
- name: circuit_id
- description: 'The Unique Identifier for the circuit where the race will be held.'
- required: true
- example: 1234-1234-1234-1234
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- race_date:
- custom: []
- name: race_date
- description: 'The date and time the race takes place, RFC 3339 in local timezone.'
- required: true
- example: '2024-05-26T14:53:59'
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- season:
- custom: []
- name: season
- description: 'The season year for this race.'
- required: true
- example: '2024'
- type: string
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- driver_ids:
- custom: []
- name: driver_ids
- description: 'An array of Unique Identifiers for drivers participating in the race.'
- required: false
- example:
- - 5678-5678-5678-5678
- - 6789-6789-6789-6789
- type: 'string[]'
- enumValues: []
- exampleWasSpecified: false
- nullable: false
- deprecated: false
- cleanBodyParameters:
- name: 'Monaco Grand Prix'
- circuit_id: 1234-1234-1234-1234
- race_date: '2024-05-26T14:53:59'
- season: '2024'
- driver_ids:
- - 5678-5678-5678-5678
- - 6789-6789-6789-6789
- fileParameters: []
- responses: []
- responseFields: []
- auth: []
- controller: null
- method: null
- route: null
diff --git a/frameworks-laravel/.scribe/endpoints/custom.0.yaml b/frameworks-laravel/.scribe/endpoints/custom.0.yaml
deleted file mode 100644
index 4b02352..0000000
--- a/frameworks-laravel/.scribe/endpoints/custom.0.yaml
+++ /dev/null
@@ -1,53 +0,0 @@
-# To include an endpoint that isn't a part of your Laravel app (or belongs to a vendor package),
-# you can define it in a custom.*.yaml file, like this one.
-# Each custom file should contain an array of endpoints. Here's an example:
-# See https://scribe.knuckles.wtf/laravel/documenting/custom-endpoints#extra-sorting-groups-in-custom-endpoint-files for more options
-
-#- httpMethods:
-# - POST
-# uri: api/doSomething/{param}
-# metadata:
-# groupName: The group the endpoint belongs to. Can be a new group or an existing group.
-# groupDescription: A description for the group. You don't need to set this for every endpoint; once is enough.
-# subgroup: You can add a subgroup, too.
-# title: Do something
-# description: 'This endpoint allows you to do something.'
-# authenticated: false
-# headers:
-# Content-Type: application/json
-# Accept: application/json
-# urlParameters:
-# param:
-# name: param
-# description: A URL param for no reason.
-# required: true
-# example: 2
-# type: integer
-# queryParameters:
-# speed:
-# name: speed
-# description: How fast the thing should be done. Can be `slow` or `fast`.
-# required: false
-# example: fast
-# type: string
-# bodyParameters:
-# something:
-# name: something
-# description: The things we should do.
-# required: true
-# example:
-# - string 1
-# - string 2
-# type: 'string[]'
-# responses:
-# - status: 200
-# description: 'When the thing was done smoothly.'
-# content: # Your response content can be an object, an array, a string or empty.
-# {
-# "hey": "ho ho ho"
-# }
-# responseFields:
-# hey:
-# name: hey
-# description: Who knows?
-# type: string # This is optional
diff --git a/frameworks-laravel/.scribe/intro.md b/frameworks-laravel/.scribe/intro.md
deleted file mode 100644
index e89564d..0000000
--- a/frameworks-laravel/.scribe/intro.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Introduction
-
- API for managing Formula 1 races, drivers, and circuits
-
-
-
- Welcome to the F1 Race API documentation!
- This documentation aims to provide all the information you need to work with our API.
-
From 8f49f3ab41a0582aeac12c4538f9e056519b4c4f Mon Sep 17 00:00:00 2001
From: Phil Sturgeon <67381+philsturgeon@users.noreply.github.com>
Date: Sun, 16 Nov 2025 16:52:01 +0000
Subject: [PATCH 5/5] add tags
---
frameworks-laravel/app/Http/Controllers/Api/RaceController.php | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frameworks-laravel/app/Http/Controllers/Api/RaceController.php b/frameworks-laravel/app/Http/Controllers/Api/RaceController.php
index 985ccb0..ebcbe08 100644
--- a/frameworks-laravel/app/Http/Controllers/Api/RaceController.php
+++ b/frameworks-laravel/app/Http/Controllers/Api/RaceController.php
@@ -7,8 +7,9 @@
use App\Http\Resources\RaceCollection;
use App\Models\Race;
use Illuminate\Http\Request;
-use Knuckles\Scribe\Attributes\{Authenticated, BodyParam, QueryParam};
+use Knuckles\Scribe\Attributes\{Authenticated, Group, BodyParam, QueryParam};
+#[Group(name: 'Races', description: 'A series of endpoints that allow programmatic access to managing F1 races.', authenticated: true)]
class RaceController extends Controller
{
/**