Skip to content

Commit 78e25fb

Browse files
authored
Add OpenAPI test coverage for capabilities (#1993)
1 parent 6ce7522 commit 78e25fb

20 files changed

Lines changed: 732 additions & 190 deletions

File tree

src/JsonApiDotNetCore.OpenApi.Swashbuckle/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public JsonApiEndpointMetadata Get(ActionDescriptor descriptor)
9292
JsonApiEndpoints.Post => GetPostResourceRequestMetadata(primaryResourceType.ClrType),
9393
JsonApiEndpoints.Patch => GetPatchResourceRequestMetadata(primaryResourceType.ClrType),
9494
JsonApiEndpoints.PostRelationship or JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.DeleteRelationship => GetRelationshipRequestMetadata(
95-
primaryResourceType.Relationships, endpoint != JsonApiEndpoints.PatchRelationship),
95+
endpoint, primaryResourceType.Relationships),
9696
_ => null
9797
};
9898
}
@@ -111,16 +111,29 @@ private static PrimaryRequestMetadata GetPatchResourceRequestMetadata(Type resou
111111
return new PrimaryRequestMetadata(documentType);
112112
}
113113

114-
private RelationshipRequestMetadata GetRelationshipRequestMetadata(IReadOnlyCollection<RelationshipAttribute> relationships, bool ignoreHasOneRelationships)
114+
private RelationshipRequestMetadata GetRelationshipRequestMetadata(JsonApiEndpoints endpoint, IReadOnlyCollection<RelationshipAttribute> relationships)
115115
{
116-
IEnumerable<RelationshipAttribute> relationshipsOfEndpoint = ignoreHasOneRelationships ? relationships.OfType<HasManyAttribute>() : relationships;
116+
IEnumerable<RelationshipAttribute> relationshipsOfEndpoint = FilterRelationshipsForEndpoint(endpoint, relationships);
117117

118118
Dictionary<RelationshipAttribute, Type> documentTypesByRelationship = relationshipsOfEndpoint.ToDictionary(relationship => relationship,
119119
_nonPrimaryDocumentTypeFactory.GetForRelationshipRequest);
120120

121121
return new RelationshipRequestMetadata(documentTypesByRelationship.AsReadOnly());
122122
}
123123

124+
private static IEnumerable<RelationshipAttribute> FilterRelationshipsForEndpoint(JsonApiEndpoints endpoint,
125+
IReadOnlyCollection<RelationshipAttribute> relationships)
126+
{
127+
return endpoint switch
128+
{
129+
JsonApiEndpoints.GetRelationship => relationships.Where(relationship => !relationship.IsViewBlocked()),
130+
JsonApiEndpoints.PatchRelationship => relationships.Where(relationship => !relationship.IsSetBlocked()),
131+
JsonApiEndpoints.PostRelationship => relationships.OfType<HasManyAttribute>().Where(relationship => !relationship.IsAddBlocked()),
132+
JsonApiEndpoints.DeleteRelationship => relationships.OfType<HasManyAttribute>().Where(relationship => !relationship.IsRemoveBlocked()),
133+
_ => relationships
134+
};
135+
}
136+
124137
private IJsonApiResponseMetadata? GetResponseMetadata(JsonApiEndpoints endpoint, ResourceType primaryResourceType,
125138
ReadOnlyCollection<HttpStatusCode> successStatusCodes, ReadOnlyCollection<HttpStatusCode> errorStatusCodes)
126139
{
@@ -131,11 +144,9 @@ private RelationshipRequestMetadata GetRelationshipRequestMetadata(IReadOnlyColl
131144
successStatusCodes, errorStatusCodes),
132145
JsonApiEndpoints.Delete => GetEmptyPrimaryResponseMetadata(successStatusCodes, errorStatusCodes),
133146
JsonApiEndpoints.GetSecondary => GetSecondaryResponseMetadata(primaryResourceType.Relationships, successStatusCodes, errorStatusCodes),
134-
JsonApiEndpoints.GetRelationship => GetRelationshipResponseMetadata(primaryResourceType.Relationships, false, successStatusCodes, errorStatusCodes),
135-
JsonApiEndpoints.PatchRelationship => GetEmptyRelationshipResponseMetadata(primaryResourceType.Relationships, false, successStatusCodes,
136-
errorStatusCodes),
137-
JsonApiEndpoints.PostRelationship or JsonApiEndpoints.DeleteRelationship => GetEmptyRelationshipResponseMetadata(primaryResourceType.Relationships,
138-
true, successStatusCodes, errorStatusCodes),
147+
JsonApiEndpoints.GetRelationship => GetRelationshipResponseMetadata(primaryResourceType.Relationships, successStatusCodes, errorStatusCodes),
148+
JsonApiEndpoints.PatchRelationship or JsonApiEndpoints.PostRelationship or JsonApiEndpoints.DeleteRelationship =>
149+
GetEmptyRelationshipResponseMetadata(endpoint, primaryResourceType.Relationships, successStatusCodes, errorStatusCodes),
139150
_ => null
140151
};
141152
}
@@ -238,24 +249,23 @@ private SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable<Relat
238249
}
239250

240251
private RelationshipResponseMetadata GetRelationshipResponseMetadata(IReadOnlyCollection<RelationshipAttribute> relationships,
241-
bool ignoreHasOneRelationships, ReadOnlyCollection<HttpStatusCode> successStatusCodes, ReadOnlyCollection<HttpStatusCode> errorStatusCodes)
252+
ReadOnlyCollection<HttpStatusCode> successStatusCodes, ReadOnlyCollection<HttpStatusCode> errorStatusCodes)
242253
{
243-
IReadOnlyCollection<RelationshipAttribute> relationshipsOfEndpoint =
244-
ignoreHasOneRelationships ? relationships.OfType<HasManyAttribute>().ToList().AsReadOnly() : relationships;
254+
IEnumerable<RelationshipAttribute> relationshipsOfEndpoint = FilterRelationshipsForEndpoint(JsonApiEndpoints.GetRelationship, relationships);
245255

246256
Dictionary<RelationshipAttribute, Type> documentTypesByRelationship = relationshipsOfEndpoint.ToDictionary(relationship => relationship,
247257
_nonPrimaryDocumentTypeFactory.GetForRelationshipResponse);
248258

249259
return new RelationshipResponseMetadata(documentTypesByRelationship.AsReadOnly(), successStatusCodes, errorStatusCodes);
250260
}
251261

252-
private static EmptyRelationshipResponseMetadata GetEmptyRelationshipResponseMetadata(IReadOnlyCollection<RelationshipAttribute> relationships,
253-
bool ignoreHasOneRelationships, ReadOnlyCollection<HttpStatusCode> successStatusCodes, ReadOnlyCollection<HttpStatusCode> errorStatusCodes)
262+
private static EmptyRelationshipResponseMetadata GetEmptyRelationshipResponseMetadata(JsonApiEndpoints endpoint,
263+
IReadOnlyCollection<RelationshipAttribute> relationships, ReadOnlyCollection<HttpStatusCode> successStatusCodes,
264+
ReadOnlyCollection<HttpStatusCode> errorStatusCodes)
254265
{
255-
IReadOnlyCollection<RelationshipAttribute> relationshipsOfEndpoint =
256-
ignoreHasOneRelationships ? relationships.OfType<HasManyAttribute>().ToList().AsReadOnly() : relationships;
266+
IEnumerable<RelationshipAttribute> relationshipsOfEndpoint = FilterRelationshipsForEndpoint(endpoint, relationships);
257267

258-
return new EmptyRelationshipResponseMetadata(relationshipsOfEndpoint, successStatusCodes, errorStatusCodes);
268+
return new EmptyRelationshipResponseMetadata(relationshipsOfEndpoint.ToArray().AsReadOnly(), successStatusCodes, errorStatusCodes);
259269
}
260270

261271
private JsonApiEndpointMetadata GetCustomMetadata(ActionDescriptor descriptor, ResourceType controllerResourceType)
@@ -281,19 +291,18 @@ private JsonApiEndpointMetadata GetCustomMetadata(ActionDescriptor descriptor, R
281291
ConsistencyGuard.ThrowIf(actionMethod == null);
282292

283293
HashSet<string> httpMethods = actionMethod.GetCustomAttributes<HttpMethodAttribute>(true).SelectMany(httpMethod => httpMethod.HttpMethods).ToHashSet();
284-
bool skipHasOneAtRelationshipEndpoint = httpMethods.Any(httpMethod => HttpMethods.IsPost(httpMethod) || HttpMethods.IsDelete(httpMethod));
285294

286295
IJsonApiRequestMetadata? requestMetadata = GetCustomRequestMetadata(descriptor, controllerResourceType, hasParameterForId,
287-
hasParameterForRelationshipName, skipHasOneAtRelationshipEndpoint);
296+
hasParameterForRelationshipName, httpMethods);
288297

289298
IJsonApiResponseMetadata? responseMetadata = GetCustomResponseMetadata(descriptor, controllerResourceType, hasParameterForRelationshipName,
290-
hasRelationshipsInRoute, skipHasOneAtRelationshipEndpoint);
299+
hasRelationshipsInRoute, httpMethods);
291300

292301
return new JsonApiEndpointMetadata(requestMetadata, responseMetadata);
293302
}
294303

295304
private IJsonApiRequestMetadata? GetCustomRequestMetadata(ActionDescriptor descriptor, ResourceType controllerResourceType, bool hasParameterForId,
296-
bool hasParameterForRelationshipName, bool skipHasOneAtRelationshipEndpoint)
305+
bool hasParameterForRelationshipName, HashSet<string> httpMethods)
297306
{
298307
ConsumesAttribute? consumes = descriptor.FilterDescriptors.Select(filter => filter.Filter).OfType<ConsumesAttribute>().FirstOrDefault();
299308

@@ -310,14 +319,19 @@ private JsonApiEndpointMetadata GetCustomMetadata(ActionDescriptor descriptor, R
310319
: GetPostResourceRequestMetadata(primaryResourceType.ClrType);
311320
}
312321

313-
return GetRelationshipRequestMetadata(primaryResourceType.Relationships, skipHasOneAtRelationshipEndpoint);
322+
JsonApiEndpoints? relationshipEndpoint = InferRelationshipEndpoint(httpMethods);
323+
324+
if (relationshipEndpoint != null)
325+
{
326+
return GetRelationshipRequestMetadata(relationshipEndpoint.Value, primaryResourceType.Relationships);
327+
}
314328
}
315329

316330
return null;
317331
}
318332

319333
private IJsonApiResponseMetadata? GetCustomResponseMetadata(ActionDescriptor descriptor, ResourceType controllerResourceType,
320-
bool hasParameterForRelationshipName, bool hasRelationshipsInRoute, bool skipHasOneAtRelationshipEndpoint)
334+
bool hasParameterForRelationshipName, bool hasRelationshipsInRoute, HashSet<string> httpMethods)
321335
{
322336
ResourceType? successResponseBodyType = null;
323337
bool isResponseBodyCollection = false;
@@ -363,10 +377,43 @@ private JsonApiEndpointMetadata GetCustomMetadata(ActionDescriptor descriptor, R
363377

364378
if (hasParameterForRelationshipName && hasRelationshipsInRoute)
365379
{
366-
return successResponseBodyType != null
367-
? GetRelationshipResponseMetadata(primaryResourceType.Relationships, skipHasOneAtRelationshipEndpoint, successStatusCodes, errorStatusCodes)
368-
: GetEmptyRelationshipResponseMetadata(primaryResourceType.Relationships, skipHasOneAtRelationshipEndpoint, successStatusCodes,
380+
if (successResponseBodyType != null)
381+
{
382+
return GetRelationshipResponseMetadata(primaryResourceType.Relationships, successStatusCodes, errorStatusCodes);
383+
}
384+
385+
JsonApiEndpoints? relationshipEndpoint = InferRelationshipEndpoint(httpMethods);
386+
387+
if (relationshipEndpoint != null)
388+
{
389+
return GetEmptyRelationshipResponseMetadata(relationshipEndpoint.Value, primaryResourceType.Relationships, successStatusCodes,
369390
errorStatusCodes);
391+
}
392+
}
393+
394+
return null;
395+
}
396+
397+
private static JsonApiEndpoints? InferRelationshipEndpoint(HashSet<string> httpMethods)
398+
{
399+
if (httpMethods.All(HttpMethods.IsPost))
400+
{
401+
return JsonApiEndpoints.PostRelationship;
402+
}
403+
404+
if (httpMethods.All(HttpMethods.IsPatch))
405+
{
406+
return JsonApiEndpoints.PatchRelationship;
407+
}
408+
409+
if (httpMethods.All(HttpMethods.IsDelete))
410+
{
411+
return JsonApiEndpoints.DeleteRelationship;
412+
}
413+
414+
if (httpMethods.All(method => HttpMethods.IsGet(method) || HttpMethods.IsHead(method)))
415+
{
416+
return JsonApiEndpoints.GetRelationship;
370417
}
371418

372419
return null;

src/JsonApiDotNetCore.OpenApi.Swashbuckle/SchemaGenerators/Documents/AtomicOperationsDocumentSchemaGenerator.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ private static bool IsToOneRelationshipEnabled(HasOneAttribute relationship, Wri
433433

434434
if (writeOperation == WriteOperationKind.SetRelationship)
435435
{
436-
isEnabled = relationship.Capabilities.HasFlag(HasOneCapabilities.AllowSet);
436+
isEnabled = !relationship.IsSetBlocked();
437437
}
438438

439439
ConsistencyGuard.ThrowIf(isEnabled == null);
@@ -446,15 +446,15 @@ private static bool IsToManyRelationshipEnabled(HasManyAttribute relationship, W
446446

447447
if (writeOperation == WriteOperationKind.SetRelationship)
448448
{
449-
isEnabled = relationship.Capabilities.HasFlag(HasManyCapabilities.AllowSet);
449+
isEnabled = !relationship.IsSetBlocked();
450450
}
451451
else if (writeOperation == WriteOperationKind.AddToRelationship)
452452
{
453-
isEnabled = relationship.Capabilities.HasFlag(HasManyCapabilities.AllowAdd);
453+
isEnabled = !relationship.IsAddBlocked();
454454
}
455455
else if (writeOperation == WriteOperationKind.RemoveFromRelationship)
456456
{
457-
isEnabled = relationship.Capabilities.HasFlag(HasManyCapabilities.AllowRemove);
457+
isEnabled = !relationship.IsRemoveBlocked();
458458
}
459459

460460
ConsistencyGuard.ThrowIf(isEnabled == null);

src/JsonApiDotNetCore.OpenApi.Swashbuckle/SwaggerComponents/ResourceFieldSchemaBuilder.cs

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,21 +185,68 @@ public void SetMembersOfRelationships(OpenApiSchema inlineSchemaForRelationships
185185
ArgumentNullException.ThrowIfNull(schemaRepository);
186186
AssertHasNoProperties(inlineSchemaForRelationships);
187187

188+
HasOneCapabilities hasOneRequiredCapability = GetRequiredCapabilityForHasOneRelationships(_resourceSchemaType.SchemaOpenType);
189+
HasManyCapabilities hasManyRequiredCapability = GetRequiredCapabilityForHasManyRelationships(_resourceSchemaType.SchemaOpenType);
190+
188191
foreach (string publicName in _schemasForResourceFields.Keys)
189192
{
190193
RelationshipAttribute? matchingRelationship = _resourceSchemaType.ResourceType.FindRelationshipByPublicName(publicName);
191194

192195
if (matchingRelationship != null)
193196
{
194-
Type identifierSchemaOpenType = forRequestSchema ? typeof(IdentifierInRequest<>) : typeof(IdentifierInResponse<>);
195-
Type identifierSchemaConstructedType = identifierSchemaOpenType.MakeGenericType(matchingRelationship.RightType.ClrType);
197+
bool hasRequiredCapability = matchingRelationship switch
198+
{
199+
HasOneAttribute hasOneRelationship => hasOneRelationship.Capabilities.HasFlag(hasOneRequiredCapability),
200+
HasManyAttribute hasManyRelationship => hasManyRelationship.Capabilities.HasFlag(hasManyRequiredCapability),
201+
_ => throw new InvalidOperationException($"Unknown relationship type '{matchingRelationship.GetType().Name}'.")
202+
};
203+
204+
if (hasRequiredCapability)
205+
{
206+
Type identifierSchemaOpenType = forRequestSchema ? typeof(IdentifierInRequest<>) : typeof(IdentifierInResponse<>);
207+
Type identifierSchemaConstructedType = identifierSchemaOpenType.MakeGenericType(matchingRelationship.RightType.ClrType);
196208

197-
_ = _dataSchemaGenerator.GenerateSchema(identifierSchemaConstructedType, forRequestSchema, schemaRepository);
198-
AddRelationshipSchemaToResourceData(matchingRelationship, inlineSchemaForRelationships, schemaRepository);
209+
_ = _dataSchemaGenerator.GenerateSchema(identifierSchemaConstructedType, forRequestSchema, schemaRepository);
210+
AddRelationshipSchemaToResourceData(matchingRelationship, inlineSchemaForRelationships, schemaRepository);
211+
}
199212
}
200213
}
201214
}
202215

216+
private static HasOneCapabilities GetRequiredCapabilityForHasOneRelationships(Type resourceDataOpenType)
217+
{
218+
HasOneCapabilities? capabilities = null;
219+
220+
if (resourceDataOpenType == typeof(DataInResponse<>))
221+
{
222+
capabilities = HasOneCapabilities.AllowView;
223+
}
224+
else if (resourceDataOpenType == typeof(DataInCreateRequest<>) || resourceDataOpenType == typeof(DataInUpdateRequest<>))
225+
{
226+
capabilities = HasOneCapabilities.AllowSet;
227+
}
228+
229+
ConsistencyGuard.ThrowIf(capabilities == null);
230+
return capabilities.Value;
231+
}
232+
233+
private static HasManyCapabilities GetRequiredCapabilityForHasManyRelationships(Type resourceDataOpenType)
234+
{
235+
HasManyCapabilities? capabilities = null;
236+
237+
if (resourceDataOpenType == typeof(DataInResponse<>))
238+
{
239+
capabilities = HasManyCapabilities.AllowView;
240+
}
241+
else if (resourceDataOpenType == typeof(DataInCreateRequest<>) || resourceDataOpenType == typeof(DataInUpdateRequest<>))
242+
{
243+
capabilities = HasManyCapabilities.AllowSet;
244+
}
245+
246+
ConsistencyGuard.ThrowIf(capabilities == null);
247+
return capabilities.Value;
248+
}
249+
203250
private void AddRelationshipSchemaToResourceData(RelationshipAttribute relationship, OpenApiSchema inlineSchemaForRelationships,
204251
SchemaRepository schemaRepository)
205252
{

src/JsonApiDotNetCore/Resources/Annotations/CapabilitiesExtensions.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,22 @@ public static bool IsSetBlocked(this RelationshipAttribute relationship)
4242
_ => false
4343
};
4444
}
45+
46+
public static bool IsAddBlocked(this RelationshipAttribute relationship)
47+
{
48+
return relationship switch
49+
{
50+
HasManyAttribute hasManyRelationship => !hasManyRelationship.Capabilities.HasFlag(HasManyCapabilities.AllowAdd),
51+
_ => false
52+
};
53+
}
54+
55+
public static bool IsRemoveBlocked(this RelationshipAttribute relationship)
56+
{
57+
return relationship switch
58+
{
59+
HasManyAttribute hasManyRelationship => !hasManyRelationship.Capabilities.HasFlag(HasManyCapabilities.AllowRemove),
60+
_ => false
61+
};
62+
}
4563
}

src/JsonApiDotNetCore/Serialization/Request/Adapters/ResourceIdentityAdapter.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ private static void AssertSetRelationshipNotBlocked(RelationshipAttribute relati
300300

301301
private static void AssertAddToRelationshipNotBlocked(HasManyAttribute relationship, RequestAdapterState state)
302302
{
303-
if (!relationship.Capabilities.HasFlag(HasManyCapabilities.AllowAdd))
303+
if (relationship.IsAddBlocked())
304304
{
305305
throw new ModelConversionException(state.Position, "Relationship cannot be added to.",
306306
$"The relationship '{relationship}' on resource type '{relationship.LeftType}' cannot be added to.");
@@ -309,7 +309,7 @@ private static void AssertAddToRelationshipNotBlocked(HasManyAttribute relations
309309

310310
private static void AssertRemoveFromRelationshipNotBlocked(HasManyAttribute relationship, RequestAdapterState state)
311311
{
312-
if (!relationship.Capabilities.HasFlag(HasManyCapabilities.AllowRemove))
312+
if (relationship.IsRemoveBlocked())
313313
{
314314
throw new ModelConversionException(state.Position, "Relationship cannot be removed from.",
315315
$"The relationship '{relationship}' on resource type '{relationship.LeftType}' cannot be removed from.");

src/JsonApiDotNetCore/Services/JsonApiResourceService.cs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -701,14 +701,7 @@ private void AssertHasRelationship([SysNotNull] RelationshipAttribute? relations
701701
[AssertionMethod]
702702
private void AssertCanViewRelationship(RelationshipAttribute relationship)
703703
{
704-
bool allowView = relationship switch
705-
{
706-
HasOneAttribute hasOneRelationship when !hasOneRelationship.Capabilities.HasFlag(HasOneCapabilities.AllowView) => false,
707-
HasManyAttribute hasManyRelationship when !hasManyRelationship.Capabilities.HasFlag(HasManyCapabilities.AllowView) => false,
708-
_ => true
709-
};
710-
711-
if (!allowView)
704+
if (relationship.IsViewBlocked())
712705
{
713706
throw new BlockedGetRelationshipException(relationship);
714707
}

0 commit comments

Comments
 (0)