Skip to content

Commit 7fd86a6

Browse files
turegjorupclaude
andcommitted
6869: Improve OpenAPI spec with descriptions, examples, and error codes
Add proper documentation to the auto-generated OpenAPI spec: - API title, description, and version in api_platform.yaml - Operation summary and description for the POST endpoint - Field-level descriptions, examples, and enum for DetectionResult - 401/403 error responses for authentication/authorization failures - Server URL from COMPOSE_SERVER_DOMAIN/COMPOSE_DOMAIN env vars - Security scheme description documenting the Apikey header format Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bac1888 commit 7fd86a6

5 files changed

Lines changed: 142 additions & 25 deletions

File tree

config/packages/api_platform.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
api_platform:
2+
title: 'ITKsites Detection API'
3+
description: 'REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.'
4+
version: '1.0.0'
5+
26
mapping:
37
paths: ['%kernel.project_dir%/src/Entity']
48
formats:

public/api-spec-v1.json

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"openapi": "3.1.0",
33
"info": {
4-
"title": "",
5-
"description": "",
6-
"version": "0.0.0"
4+
"title": "ITKsites Detection API",
5+
"description": "REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.",
6+
"version": "1.0.0"
77
},
88
"servers": [
99
{
10-
"url": "/",
10+
"url": "https://itksites.local.itkdev.dk",
1111
"description": ""
1212
}
1313
],
@@ -20,11 +20,11 @@
2020
],
2121
"responses": {
2222
"202": {
23-
"description": "DetectionResult resource created",
23+
"description": "Detection result accepted for processing",
2424
"links": {}
2525
},
2626
"400": {
27-
"description": "Invalid input",
27+
"description": "Invalid input \u2014 malformed request body",
2828
"content": {
2929
"application/ld+json": {
3030
"schema": {
@@ -44,8 +44,14 @@
4444
},
4545
"links": {}
4646
},
47+
"401": {
48+
"description": "Unauthorized \u2014 missing or invalid API key. The Authorization header must use the format: Apikey {key}"
49+
},
50+
"403": {
51+
"description": "Forbidden \u2014 the authenticated server does not have the required ROLE_SERVER role"
52+
},
4753
"422": {
48-
"description": "An error occurred",
54+
"description": "Validation error \u2014 one or more fields failed constraint validation",
4955
"content": {
5056
"application/ld+json": {
5157
"schema": {
@@ -66,8 +72,8 @@
6672
"links": {}
6773
}
6874
},
69-
"summary": "Creates a DetectionResult resource.",
70-
"description": "Creates a DetectionResult resource.",
75+
"summary": "Submit a detection result for async processing",
76+
"description": "Accepts a detection result from the server harvester and queues it for asynchronous processing. The result is deduplicated by content hash \u2014 identical submissions update the last contact timestamp without triggering reprocessing. Returns 202 Accepted with an empty body.",
7177
"parameters": [],
7278
"requestBody": {
7379
"description": "The new DetectionResult resource",
@@ -228,15 +234,25 @@
228234
"type": "object",
229235
"properties": {
230236
"type": {
231-
"default": "",
232-
"type": "string"
237+
"enum": [
238+
"dir",
239+
"docker",
240+
"drupal",
241+
"git",
242+
"nginx",
243+
"symfony"
244+
]
233245
},
234246
"rootDir": {
247+
"description": "Absolute path to the root directory of the detected installation on the server",
235248
"default": "",
249+
"example": "/data/www/example-site/htdocs",
236250
"type": "string"
237251
},
238252
"data": {
253+
"description": "JSON-encoded payload from the server harvester containing the detection details. Structure varies by type.",
239254
"default": "",
255+
"example": "{\"packages\":{\"symfony/framework-bundle\":{\"version\":\"7.2.1\"}}}",
240256
"type": "string"
241257
}
242258
}
@@ -347,7 +363,7 @@
347363
"securitySchemes": {
348364
"apiKey": {
349365
"type": "apiKey",
350-
"description": "Value for the Authorization header parameter.",
366+
"description": "Server API key. Use the format: Apikey {your-api-key}",
351367
"name": "Authorization",
352368
"in": "header"
353369
}

public/api-spec-v1.yaml

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
openapi: 3.1.0
22
info:
3-
title: ''
4-
description: ''
5-
version: 0.0.0
3+
title: 'ITKsites Detection API'
4+
description: 'REST API for ingesting server detection results from the ITK sites server harvester. Detection results are processed asynchronously to track servers, sites, domains, packages, modules, Docker images, and git repositories.'
5+
version: 1.0.0
66
servers:
77
-
8-
url: /
8+
url: 'https://itksites.local.itkdev.dk'
99
description: ''
1010
paths:
1111
/api/detection_results:
@@ -15,10 +15,10 @@ paths:
1515
- DetectionResult
1616
responses:
1717
'202':
18-
description: 'DetectionResult resource created'
18+
description: 'Detection result accepted for processing'
1919
links: { }
2020
'400':
21-
description: 'Invalid input'
21+
description: 'Invalid input — malformed request body'
2222
content:
2323
application/ld+json:
2424
schema:
@@ -30,8 +30,12 @@ paths:
3030
schema:
3131
$ref: '#/components/schemas/Error'
3232
links: { }
33+
'401':
34+
description: 'Unauthorized — missing or invalid API key. The Authorization header must use the format: Apikey {key}'
35+
'403':
36+
description: 'Forbidden — the authenticated server does not have the required ROLE_SERVER role'
3337
'422':
34-
description: 'An error occurred'
38+
description: 'Validation error — one or more fields failed constraint validation'
3539
content:
3640
application/ld+json:
3741
schema:
@@ -43,8 +47,8 @@ paths:
4347
schema:
4448
$ref: '#/components/schemas/ConstraintViolation'
4549
links: { }
46-
summary: 'Creates a DetectionResult resource.'
47-
description: 'Creates a DetectionResult resource.'
50+
summary: 'Submit a detection result for async processing'
51+
description: 'Accepts a detection result from the server harvester and queues it for asynchronous processing. The result is deduplicated by content hash — identical submissions update the last contact timestamp without triggering reprocessing. Returns 202 Accepted with an empty body.'
4852
parameters: []
4953
requestBody:
5054
description: 'The new DetectionResult resource'
@@ -159,13 +163,22 @@ components:
159163
type: object
160164
properties:
161165
type:
162-
default: ''
163-
type: string
166+
enum:
167+
- dir
168+
- docker
169+
- drupal
170+
- git
171+
- nginx
172+
- symfony
164173
rootDir:
174+
description: 'Absolute path to the root directory of the detected installation on the server'
165175
default: ''
176+
example: /data/www/example-site/htdocs
166177
type: string
167178
data:
179+
description: 'JSON-encoded payload from the server harvester containing the detection details. Structure varies by type.'
168180
default: ''
181+
example: '{"packages":{"symfony/framework-bundle":{"version":"7.2.1"}}}'
169182
type: string
170183
Error:
171184
type: object
@@ -246,7 +259,7 @@ components:
246259
securitySchemes:
247260
apiKey:
248261
type: apiKey
249-
description: 'Value for the Authorization header parameter.'
262+
description: 'Server API key. Use the format: Apikey {your-api-key}'
250263
name: Authorization
251264
in: header
252265
security:

src/Entity/DetectionResult.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,44 @@
44

55
namespace App\Entity;
66

7+
use ApiPlatform\Metadata\ApiProperty;
78
use ApiPlatform\Metadata\ApiResource;
89
use ApiPlatform\Metadata\Post;
10+
use ApiPlatform\OpenApi\Model;
911
use App\Repository\DetectionResultRepository;
12+
use App\Types\DetectionType;
1013
use App\Utils\RootDirNormalizer;
1114
use Doctrine\ORM\Mapping as ORM;
1215
use Symfony\Component\Serializer\Attribute\Groups;
1316

1417
#[ApiResource(
1518
operations: [
16-
new Post(status: 202, output: false, messenger: true),
19+
new Post(
20+
status: 202,
21+
output: false,
22+
messenger: true,
23+
openapi: new Model\Operation(
24+
summary: 'Submit a detection result for async processing',
25+
description: 'Accepts a detection result from the server harvester and queues it for asynchronous processing. The result is deduplicated by content hash — identical submissions update the last contact timestamp without triggering reprocessing. Returns 202 Accepted with an empty body.',
26+
responses: [
27+
'202' => new Model\Response(
28+
description: 'Detection result accepted for processing',
29+
),
30+
'400' => new Model\Response(
31+
description: 'Invalid input — malformed request body',
32+
),
33+
'401' => new Model\Response(
34+
description: 'Unauthorized — missing or invalid API key. The Authorization header must use the format: Apikey {key}',
35+
),
36+
'403' => new Model\Response(
37+
description: 'Forbidden — the authenticated server does not have the required ROLE_SERVER role',
38+
),
39+
'422' => new Model\Response(
40+
description: 'Validation error — one or more fields failed constraint validation',
41+
),
42+
],
43+
),
44+
),
1745
],
1846
denormalizationContext: ['groups' => ['write']],
1947
)]
@@ -24,10 +52,19 @@ class DetectionResult extends AbstractBaseEntity implements \Stringable
2452
{
2553
#[ORM\Column(type: 'string', length: 255)]
2654
#[Groups(['write'])]
55+
#[ApiProperty(
56+
description: 'The type of detection result, determines which handler processes the data',
57+
example: DetectionType::NGINX,
58+
schema: ['enum' => [DetectionType::DIRECTORY, DetectionType::DOCKER, DetectionType::DRUPAL, DetectionType::GIT, DetectionType::NGINX, DetectionType::SYMFONY]],
59+
)]
2760
private string $type = '';
2861

2962
#[ORM\Column(type: 'string', length: 255)]
3063
#[Groups(['write'])]
64+
#[ApiProperty(
65+
description: 'Absolute path to the root directory of the detected installation on the server',
66+
example: '/data/www/example-site/htdocs',
67+
)]
3168
private string $rootDir = '';
3269

3370
#[ORM\ManyToOne(targetEntity: Server::class, inversedBy: 'detectionResults')]
@@ -36,6 +73,10 @@ class DetectionResult extends AbstractBaseEntity implements \Stringable
3673

3774
#[ORM\Column(type: 'text')]
3875
#[Groups(['write'])]
76+
#[ApiProperty(
77+
description: 'JSON-encoded payload from the server harvester containing the detection details. Structure varies by type.',
78+
example: '{"packages":{"symfony/framework-bundle":{"version":"7.2.1"}}}',
79+
)]
3980
private string $data = '';
4081

4182
#[ORM\Column(type: 'string', length: 255, unique: true)]

src/OpenApi/OpenApiFactory.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\OpenApi;
6+
7+
use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
8+
use ApiPlatform\OpenApi\Model\SecurityScheme;
9+
use ApiPlatform\OpenApi\Model\Server;
10+
use ApiPlatform\OpenApi\OpenApi;
11+
use Symfony\Component\DependencyInjection\Attribute\AsDecorator;
12+
use Symfony\Component\DependencyInjection\Attribute\Autowire;
13+
14+
#[AsDecorator(decorates: 'api_platform.openapi.factory')]
15+
class OpenApiFactory implements OpenApiFactoryInterface
16+
{
17+
public function __construct(
18+
private OpenApiFactoryInterface $decorated,
19+
#[Autowire('%env(default::COMPOSE_SERVER_DOMAIN)%')]
20+
private ?string $serverDomain,
21+
#[Autowire('%env(default::COMPOSE_DOMAIN)%')]
22+
private ?string $fallbackDomain,
23+
) {
24+
}
25+
26+
public function __invoke(array $context = []): OpenApi
27+
{
28+
$openApi = $this->decorated->__invoke($context);
29+
30+
$domain = ($this->serverDomain ?? '') !== '' ? $this->serverDomain : $this->fallbackDomain;
31+
32+
if (null !== $domain && '' !== $domain) {
33+
$openApi = $openApi->withServers([new Server('https://'.$domain)]);
34+
}
35+
36+
$securitySchemes = $openApi->getComponents()->getSecuritySchemes();
37+
if ($securitySchemes instanceof \ArrayObject && isset($securitySchemes['apiKey']) && $securitySchemes['apiKey'] instanceof SecurityScheme) {
38+
$securitySchemes['apiKey'] = $securitySchemes['apiKey']->withDescription('Server API key. Use the format: Apikey {your-api-key}');
39+
}
40+
41+
return $openApi;
42+
}
43+
}

0 commit comments

Comments
 (0)