Skip to content

Commit b0d17df

Browse files
committed
openapi: include ETag/Location headers
1 parent 6c9aff8 commit b0d17df

File tree

2 files changed

+61
-1
lines changed

2 files changed

+61
-1
lines changed

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiOperationDocumentationFilter.cs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using JsonApiDotNetCore.Resources;
88
using JsonApiDotNetCore.Resources.Annotations;
99
using Microsoft.AspNetCore.Mvc.ApiExplorer;
10+
using Microsoft.Net.Http.Headers;
1011
using Microsoft.OpenApi.Any;
1112
using Microsoft.OpenApi.Models;
1213
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -162,13 +163,15 @@ private static void ApplyGetPrimary(OpenApiOperation operation, ResourceType res
162163
SetOperationSummary(operation, $"Retrieves a collection of {resourceType} without returning them.");
163164
SetOperationRemarks(operation, TextCompareETag);
164165
SetResponseDescription(operation.Responses, HttpStatusCode.OK, TextCompletedSuccessfully);
166+
SetResponseHeaderETag(operation.Responses, HttpStatusCode.OK);
165167
}
166168
else
167169
{
168170
SetOperationSummary(operation, $"Retrieves a collection of {resourceType}.");
169171

170172
SetResponseDescription(operation.Responses, HttpStatusCode.OK,
171173
$"Successfully returns the found {resourceType}, or an empty array if none were found.");
174+
SetResponseHeaderETag(operation.Responses, HttpStatusCode.OK);
172175
}
173176

174177
AddQueryStringParameters(operation, false);
@@ -183,11 +186,13 @@ private static void ApplyGetPrimary(OpenApiOperation operation, ResourceType res
183186
SetOperationSummary(operation, $"Retrieves an individual {singularName} by its identifier without returning it.");
184187
SetOperationRemarks(operation, TextCompareETag);
185188
SetResponseDescription(operation.Responses, HttpStatusCode.OK, TextCompletedSuccessfully);
189+
SetResponseHeaderETag(operation.Responses, HttpStatusCode.OK);
186190
}
187191
else
188192
{
189193
SetOperationSummary(operation, $"Retrieves an individual {singularName} by its identifier.");
190194
SetResponseDescription(operation.Responses, HttpStatusCode.OK, $"Successfully returns the found {singularName}.");
195+
SetResponseHeaderETag(operation.Responses, HttpStatusCode.OK);
191196
}
192197

193198
SetParameterDescription(operation.Parameters[0], $"The identifier of the {singularName} to retrieve.");
@@ -207,6 +212,7 @@ private void ApplyPostResource(OpenApiOperation operation, ResourceType resource
207212

208213
SetResponseDescription(operation.Responses, HttpStatusCode.Created,
209214
$"The {singularName} was successfully created, which resulted in additional changes. The newly created {singularName} is returned.");
215+
SetResponseHeaderLocation(operation.Responses, HttpStatusCode.Created);
210216

211217
SetResponseDescription(operation.Responses, HttpStatusCode.NoContent,
212218
$"The {singularName} was successfully created, which did not result in additional changes.");
@@ -271,6 +277,7 @@ relationship is HasOneAttribute
271277

272278
SetOperationRemarks(operation, TextCompareETag);
273279
SetResponseDescription(operation.Responses, HttpStatusCode.OK, TextCompletedSuccessfully);
280+
SetResponseHeaderETag(operation.Responses, HttpStatusCode.OK);
274281
}
275282
else
276283
{
@@ -280,6 +287,7 @@ relationship is HasOneAttribute
280287
relationship is HasOneAttribute
281288
? $"Successfully returns the found {rightName}, or <c>null</c> if it was not found."
282289
: $"Successfully returns the found {rightName}, or an empty array if none were found.");
290+
SetResponseHeaderETag(operation.Responses, HttpStatusCode.OK);
283291
}
284292

285293
SetParameterDescription(operation.Parameters[0], $"The identifier of the {singularLeftName} whose related {rightName} to retrieve.");
@@ -303,6 +311,7 @@ relationship is HasOneAttribute
303311

304312
SetOperationRemarks(operation, TextCompareETag);
305313
SetResponseDescription(operation.Responses, HttpStatusCode.OK, TextCompletedSuccessfully);
314+
SetResponseHeaderETag(operation.Responses, HttpStatusCode.OK);
306315
}
307316
else
308317
{
@@ -313,6 +322,7 @@ relationship is HasOneAttribute
313322
relationship is HasOneAttribute
314323
? $"Successfully returns the found {singularRightName} {ident}, or <c>null</c> if it was not found."
315324
: $"Successfully returns the found {singularRightName} {ident}, or an empty array if none were found.");
325+
SetResponseHeaderETag(operation.Responses, HttpStatusCode.OK);
316326
}
317327

318328
SetParameterDescription(operation.Parameters[0], $"The identifier of the {singularLeftName} whose related {singularRightName} {ident} to retrieve.");
@@ -420,6 +430,31 @@ private static void SetRequestBodyDescription(OpenApiRequestBody requestBody, st
420430
}
421431

422432
private static void SetResponseDescription(OpenApiResponses responses, HttpStatusCode statusCode, string description)
433+
{
434+
OpenApiResponse response = GetOrCreateResponse(responses, statusCode);
435+
response.Description = XmlCommentsTextHelper.Humanize(description);
436+
}
437+
438+
private static void SetResponseHeaderETag(OpenApiResponses responses, HttpStatusCode statusCode)
439+
{
440+
OpenApiResponse response = GetOrCreateResponse(responses, statusCode);
441+
response.Headers[HeaderNames.ETag] = new OpenApiHeader
442+
{
443+
Description = "ETag identifying the version of the fetched resource.",
444+
Example = new OpenApiString("\"33a64df551425fcc55e4d42a148795d9f25f89d4\""),
445+
};
446+
}
447+
448+
private static void SetResponseHeaderLocation(OpenApiResponses responses, HttpStatusCode statusCode)
449+
{
450+
OpenApiResponse response = GetOrCreateResponse(responses, statusCode);
451+
response.Headers[HeaderNames.Location] = new OpenApiHeader
452+
{
453+
Description = "Location of the newly created resource.",
454+
};
455+
}
456+
457+
private static OpenApiResponse GetOrCreateResponse(OpenApiResponses responses, HttpStatusCode statusCode)
423458
{
424459
string responseCode = ((int)statusCode).ToString();
425460

@@ -429,7 +464,7 @@ private static void SetResponseDescription(OpenApiResponses responses, HttpStatu
429464
responses.Add(responseCode, response);
430465
}
431466

432-
response.Description = XmlCommentsTextHelper.Humanize(description);
467+
return response;
433468
}
434469

435470
private static void AddQueryStringParameters(OpenApiOperation operation, bool isRelationshipEndpoint)

test/OpenApiTests/DocComments/DocCommentsTests.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ public async Task Endpoints_are_documented()
7676
{
7777
responseElement.EnumerateObject().ShouldHaveCount(2);
7878
responseElement.Should().HaveProperty("200.description", "Successfully returns the found skyscrapers, or an empty array if none were found.");
79+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
80+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
7981
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
8082
});
8183
});
@@ -96,6 +98,8 @@ public async Task Endpoints_are_documented()
9698
{
9799
responseElement.EnumerateObject().ShouldHaveCount(2);
98100
responseElement.Should().HaveProperty("200.description", "The operation completed successfully.");
101+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
102+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
99103
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
100104
});
101105
});
@@ -115,6 +119,7 @@ public async Task Endpoints_are_documented()
115119
{
116120
responseElement.EnumerateObject().ShouldHaveCount(5);
117121
responseElement.Should().HaveProperty("201.description", "The skyscraper was successfully created, which resulted in additional changes. The newly created skyscraper is returned.");
122+
responseElement.Should().HaveProperty("201.headers.Location.description", "Location of the newly created resource.");
118123
responseElement.Should().HaveProperty("204.description", "The skyscraper was successfully created, which did not result in additional changes.");
119124
responseElement.Should().HaveProperty("400.description", "The query string is invalid or the request body is missing or malformed.");
120125
responseElement.Should().HaveProperty("409.description", "A resource type in the request body is incompatible.");
@@ -142,6 +147,8 @@ public async Task Endpoints_are_documented()
142147
{
143148
responseElement.EnumerateObject().ShouldHaveCount(3);
144149
responseElement.Should().HaveProperty("200.description", "Successfully returns the found skyscraper.");
150+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
151+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
145152
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
146153
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
147154
});
@@ -165,6 +172,8 @@ public async Task Endpoints_are_documented()
165172
{
166173
responseElement.EnumerateObject().ShouldHaveCount(3);
167174
responseElement.Should().HaveProperty("200.description", "The operation completed successfully.");
175+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
176+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
168177
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
169178
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
170179
});
@@ -236,6 +245,8 @@ public async Task Endpoints_are_documented()
236245
{
237246
responseElement.EnumerateObject().ShouldHaveCount(3);
238247
responseElement.Should().HaveProperty("200.description", "Successfully returns the found elevator, or `null` if it was not found.");
248+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
249+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
239250
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
240251
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
241252
});
@@ -259,6 +270,8 @@ public async Task Endpoints_are_documented()
259270
{
260271
responseElement.EnumerateObject().ShouldHaveCount(3);
261272
responseElement.Should().HaveProperty("200.description", "The operation completed successfully.");
273+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
274+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
262275
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
263276
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
264277
});
@@ -284,6 +297,8 @@ public async Task Endpoints_are_documented()
284297
{
285298
responseElement.EnumerateObject().ShouldHaveCount(3);
286299
responseElement.Should().HaveProperty("200.description", "Successfully returns the found elevator identity, or `null` if it was not found.");
300+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
301+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
287302
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
288303
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
289304
});
@@ -307,6 +322,8 @@ public async Task Endpoints_are_documented()
307322
{
308323
responseElement.EnumerateObject().ShouldHaveCount(3);
309324
responseElement.Should().HaveProperty("200.description", "The operation completed successfully.");
325+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
326+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
310327
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
311328
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
312329
});
@@ -355,6 +372,8 @@ public async Task Endpoints_are_documented()
355372
{
356373
responseElement.EnumerateObject().ShouldHaveCount(3);
357374
responseElement.Should().HaveProperty("200.description", "Successfully returns the found spaces, or an empty array if none were found.");
375+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
376+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
358377
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
359378
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
360379
});
@@ -378,6 +397,8 @@ public async Task Endpoints_are_documented()
378397
{
379398
responseElement.EnumerateObject().ShouldHaveCount(3);
380399
responseElement.Should().HaveProperty("200.description", "The operation completed successfully.");
400+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
401+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
381402
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
382403
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
383404
});
@@ -403,6 +424,8 @@ public async Task Endpoints_are_documented()
403424
{
404425
responseElement.EnumerateObject().ShouldHaveCount(3);
405426
responseElement.Should().HaveProperty("200.description", "Successfully returns the found space identities, or an empty array if none were found.");
427+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
428+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
406429
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
407430
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
408431
});
@@ -426,6 +449,8 @@ public async Task Endpoints_are_documented()
426449
{
427450
responseElement.EnumerateObject().ShouldHaveCount(3);
428451
responseElement.Should().HaveProperty("200.description", "The operation completed successfully.");
452+
responseElement.Should().HaveProperty("200.headers.ETag.description", "ETag identifying the version of the fetched resource.");
453+
responseElement.Should().HaveProperty("200.headers.ETag.example", "\"33a64df551425fcc55e4d42a148795d9f25f89d4\"");
429454
responseElement.Should().HaveProperty("400.description", "The query string is invalid.");
430455
responseElement.Should().HaveProperty("404.description", "The skyscraper does not exist.");
431456
});

0 commit comments

Comments
 (0)