Skip to content

Commit 1815dec

Browse files
committed
More elaborate testing
-> in sync with latest version of nullability/required table -> introduces ResourceFieldValidationMetadataProvider -> Fix test in legacy projects -> Reusable faker building block for OpenApiClient related concerns
1 parent a2e7a96 commit 1815dec

File tree

81 files changed

+9828
-4759
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

81 files changed

+9828
-4759
lines changed

src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public interface IJsonApiClient
3535
/// An <see cref="IDisposable" /> to clear the current registration. For efficient memory usage, it is recommended to wrap calls to this method in a
3636
/// <c>using</c> statement, so the registrations are cleaned up after executing the request.
3737
/// </returns>
38-
IDisposable OmitDefaultValuesForAttributesInRequestDocument<TRequestDocument, TAttributesObject>(TRequestDocument requestDocument,
38+
IDisposable WithPartialAttributeSerialization<TRequestDocument, TAttributesObject>(TRequestDocument requestDocument,
3939
params Expression<Func<TAttributesObject, object?>>[] alwaysIncludedAttributeSelectors)
4040
where TRequestDocument : class;
4141
}

src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs

Lines changed: 37 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings settings)
2424
}
2525

2626
/// <inheritdoc />
27-
public IDisposable OmitDefaultValuesForAttributesInRequestDocument<TRequestDocument, TAttributesObject>(TRequestDocument requestDocument,
27+
public IDisposable WithPartialAttributeSerialization<TRequestDocument, TAttributesObject>(TRequestDocument requestDocument,
2828
params Expression<Func<TAttributesObject, object?>>[] alwaysIncludedAttributeSelectors)
2929
where TRequestDocument : class
3030
{
@@ -40,12 +40,12 @@ public IDisposable OmitDefaultValuesForAttributesInRequestDocument<TRequestDocum
4040
}
4141
else
4242
{
43-
throw new ArgumentException($"The expression '{selector}' should select a single property. For example: 'article => article.Title'.");
43+
throw new ArgumentException(
44+
$"The expression '{nameof(alwaysIncludedAttributeSelectors)}' should select a single property. For example: 'article => article.Title'.");
4445
}
4546
}
4647

47-
_jsonApiJsonConverter.RegisterRequestDocumentForAttributesOmission(requestDocument,
48-
new AttributesObjectInfo(attributeNames, typeof(TAttributesObject)));
48+
_jsonApiJsonConverter.RegisterDocument(requestDocument, new AttributeNamesContainer(attributeNames, typeof(TAttributesObject)));
4949

5050
return new RequestDocumentRegistrationScope(_jsonApiJsonConverter, requestDocument);
5151
}
@@ -69,15 +69,15 @@ private static Expression RemoveConvert(Expression expression)
6969

7070
private sealed class JsonApiJsonConverter : JsonConverter
7171
{
72-
private readonly Dictionary<object, AttributesObjectInfo> _attributesObjectInfoByRequestDocument = new();
72+
private readonly Dictionary<object, AttributeNamesContainer> _alwaysIncludedAttributesByRequestDocument = new();
7373
private readonly Dictionary<Type, ISet<object>> _requestDocumentsByType = new();
7474
private SerializationScope? _serializationScope;
7575

7676
public override bool CanRead => false;
7777

78-
public void RegisterRequestDocumentForAttributesOmission(object requestDocument, AttributesObjectInfo attributesObjectInfo)
78+
public void RegisterDocument(object requestDocument, AttributeNamesContainer alwaysIncludedAttributes)
7979
{
80-
_attributesObjectInfoByRequestDocument[requestDocument] = attributesObjectInfo;
80+
_alwaysIncludedAttributesByRequestDocument[requestDocument] = alwaysIncludedAttributes;
8181

8282
Type requestDocumentType = requestDocument.GetType();
8383

@@ -91,9 +91,9 @@ public void RegisterRequestDocumentForAttributesOmission(object requestDocument,
9191

9292
public void RemoveRegistration(object requestDocument)
9393
{
94-
if (_attributesObjectInfoByRequestDocument.ContainsKey(requestDocument))
94+
if (_alwaysIncludedAttributesByRequestDocument.ContainsKey(requestDocument))
9595
{
96-
_attributesObjectInfoByRequestDocument.Remove(requestDocument);
96+
_alwaysIncludedAttributesByRequestDocument.Remove(requestDocument);
9797

9898
Type requestDocumentType = requestDocument.GetType();
9999
_requestDocumentsByType[requestDocumentType].Remove(requestDocument);
@@ -136,7 +136,7 @@ public override void WriteJson(JsonWriter writer, object? value, JsonSerializer
136136
}
137137
else
138138
{
139-
AttributesObjectInfo? attributesObjectInfo = _serializationScope.AttributesObjectInScope;
139+
AttributeNamesContainer? attributesObjectInfo = _serializationScope.AttributesObjectInScope;
140140

141141
AssertObjectMatchesSerializationScope(attributesObjectInfo, value);
142142

@@ -158,7 +158,7 @@ private void SerializeRequestDocument(JsonWriter writer, object value, JsonSeria
158158
{
159159
_serializationScope = new SerializationScope();
160160

161-
if (_attributesObjectInfoByRequestDocument.TryGetValue(value, out AttributesObjectInfo? attributesObjectInfo))
161+
if (_alwaysIncludedAttributesByRequestDocument.TryGetValue(value, out AttributeNamesContainer? attributesObjectInfo))
162162
{
163163
_serializationScope.AttributesObjectInScope = attributesObjectInfo;
164164
}
@@ -173,31 +173,32 @@ private void SerializeRequestDocument(JsonWriter writer, object value, JsonSeria
173173
}
174174
}
175175

176-
private static void AssertObjectMatchesSerializationScope([SysNotNull] AttributesObjectInfo? attributesObjectInfo, object value)
176+
private static void AssertObjectMatchesSerializationScope([SysNotNull] AttributeNamesContainer? attributesObjectInfo, object value)
177177
{
178178
Type objectType = value.GetType();
179179

180-
if (attributesObjectInfo == null || !attributesObjectInfo.MatchesType(objectType))
180+
if (attributesObjectInfo == null || !attributesObjectInfo.MatchesAttributesObjectType(objectType))
181181
{
182182
throw new UnreachableCodeException();
183183
}
184184
}
185185

186-
private static void SerializeAttributesObject(AttributesObjectInfo alwaysIncludedAttributes, JsonWriter writer, object value, JsonSerializer serializer)
186+
private static void SerializeAttributesObject(AttributeNamesContainer alwaysIncludedAttributes, JsonWriter writer, object value,
187+
JsonSerializer serializer)
187188
{
188189
AssertRequiredPropertiesAreNotExcluded(value, alwaysIncludedAttributes, writer);
189190

190191
serializer.ContractResolver = new JsonApiAttributeContractResolver(alwaysIncludedAttributes);
191192
serializer.Serialize(writer, value);
192193
}
193194

194-
private static void AssertRequiredPropertiesAreNotExcluded(object value, AttributesObjectInfo alwaysIncludedAttributes, JsonWriter jsonWriter)
195+
private static void AssertRequiredPropertiesAreNotExcluded(object value, AttributeNamesContainer alwaysIncludedAttributes, JsonWriter jsonWriter)
195196
{
196197
PropertyInfo[] propertyInfos = value.GetType().GetProperties();
197198

198199
foreach (PropertyInfo attributesPropertyInfo in propertyInfos)
199200
{
200-
bool isExplicitlyIncluded = alwaysIncludedAttributes.IsAttributeMarkedForInclusion(attributesPropertyInfo.Name);
201+
bool isExplicitlyIncluded = alwaysIncludedAttributes.ContainsAttribute(attributesPropertyInfo.Name);
201202

202203
if (isExplicitlyIncluded)
203204
{
@@ -212,7 +213,7 @@ private static void AssertRequiredPropertyIsNotIgnored(object value, PropertyInf
212213
{
213214
JsonPropertyAttribute jsonPropertyForAttribute = attribute.GetCustomAttributes<JsonPropertyAttribute>().Single();
214215

215-
if (jsonPropertyForAttribute.Required != Required.Always)
216+
if (jsonPropertyForAttribute.Required is not (Required.Always or Required.AllowNull))
216217
{
217218
return;
218219
}
@@ -221,8 +222,7 @@ private static void AssertRequiredPropertyIsNotIgnored(object value, PropertyInf
221222

222223
if (isPropertyIgnored)
223224
{
224-
throw new JsonSerializationException(
225-
$"Ignored property '{jsonPropertyForAttribute.PropertyName}' must have a value because it is required. Path '{path}'.");
225+
throw new InvalidOperationException($"The following property should not be omitted: {path}.{jsonPropertyForAttribute.PropertyName}.");
226226
}
227227
}
228228

@@ -248,7 +248,7 @@ private static bool DefaultValueEqualsCurrentValue(PropertyInfo propertyInfo, ob
248248
private sealed class SerializationScope
249249
{
250250
private bool _isFirstAttemptToConvertAttributes = true;
251-
public AttributesObjectInfo? AttributesObjectInScope { get; set; }
251+
public AttributeNamesContainer? AttributesObjectInScope { get; set; }
252252

253253
public bool ShouldConvertAsAttributesObject(Type type)
254254
{
@@ -257,7 +257,7 @@ public bool ShouldConvertAsAttributesObject(Type type)
257257
return false;
258258
}
259259

260-
if (!AttributesObjectInScope.MatchesType(type))
260+
if (!AttributesObjectInScope.MatchesAttributesObjectType(type))
261261
{
262262
return false;
263263
}
@@ -267,26 +267,26 @@ public bool ShouldConvertAsAttributesObject(Type type)
267267
}
268268
}
269269

270-
private sealed class AttributesObjectInfo
270+
private sealed class AttributeNamesContainer
271271
{
272-
private readonly ISet<string> _attributesMarkedForInclusion;
272+
private readonly ISet<string> _attributeNames;
273273
private readonly Type _attributesObjectType;
274274

275-
public AttributesObjectInfo(ISet<string> attributesMarkedForInclusion, Type attributesObjectType)
275+
public AttributeNamesContainer(ISet<string> attributeNames, Type attributesObjectType)
276276
{
277-
ArgumentGuard.NotNull(attributesMarkedForInclusion);
277+
ArgumentGuard.NotNull(attributeNames);
278278
ArgumentGuard.NotNull(attributesObjectType);
279279

280-
_attributesMarkedForInclusion = attributesMarkedForInclusion;
280+
_attributeNames = attributeNames;
281281
_attributesObjectType = attributesObjectType;
282282
}
283283

284-
public bool IsAttributeMarkedForInclusion(string name)
284+
public bool ContainsAttribute(string name)
285285
{
286-
return _attributesMarkedForInclusion.Contains(name);
286+
return _attributeNames.Contains(name);
287287
}
288288

289-
public bool MatchesType(Type type)
289+
public bool MatchesAttributesObjectType(Type type)
290290
{
291291
return _attributesObjectType == type;
292292
}
@@ -314,22 +314,24 @@ public void Dispose()
314314

315315
private sealed class JsonApiAttributeContractResolver : DefaultContractResolver
316316
{
317-
private readonly AttributesObjectInfo _attributesObjectInfo;
317+
private readonly AttributeNamesContainer _alwaysIncludedAttributes;
318318

319-
public JsonApiAttributeContractResolver(AttributesObjectInfo attributesObjectInfo)
319+
public JsonApiAttributeContractResolver(AttributeNamesContainer alwaysIncludedAttributes)
320320
{
321-
ArgumentGuard.NotNull(attributesObjectInfo);
321+
ArgumentGuard.NotNull(alwaysIncludedAttributes);
322322

323-
_attributesObjectInfo = attributesObjectInfo;
323+
_alwaysIncludedAttributes = alwaysIncludedAttributes;
324324
}
325325

326326
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
327327
{
328328
JsonProperty property = base.CreateProperty(member, memberSerialization);
329329

330-
if (_attributesObjectInfo.MatchesType(property.DeclaringType!))
330+
bool canOmitAttribute = property.Required != Required.Always;
331+
332+
if (canOmitAttribute && _alwaysIncludedAttributes.MatchesAttributesObjectType(property.DeclaringType!))
331333
{
332-
if (_attributesObjectInfo.IsAttributeMarkedForInclusion(property.UnderlyingName!))
334+
if (_alwaysIncludedAttributes.ContainsAttribute(property.UnderlyingName!))
333335
{
334336
property.NullValueHandling = NullValueHandling.Include;
335337
property.DefaultValueHandling = DefaultValueHandling.Include;

src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@ internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescrip
2222

2323
public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors();
2424

25-
public JsonApiActionDescriptorCollectionProvider(IControllerResourceMapping controllerResourceMapping, IActionDescriptorCollectionProvider defaultProvider)
25+
public JsonApiActionDescriptorCollectionProvider(IControllerResourceMapping controllerResourceMapping, IActionDescriptorCollectionProvider defaultProvider,
26+
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider)
2627
{
2728
ArgumentGuard.NotNull(controllerResourceMapping);
2829
ArgumentGuard.NotNull(defaultProvider);
2930

3031
_defaultProvider = defaultProvider;
31-
_jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(controllerResourceMapping);
32+
_jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(controllerResourceMapping, resourceFieldValidationMetadataProvider);
3233
}
3334

3435
private ActionDescriptorCollection GetActionDescriptors()

src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ internal sealed class JsonApiEndpointMetadataProvider
1515
{
1616
private readonly IControllerResourceMapping _controllerResourceMapping;
1717
private readonly EndpointResolver _endpointResolver = new();
18+
private readonly NonPrimaryDocumentTypeFactory _nonPrimaryDocumentTypeFactory;
1819

19-
public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping)
20+
public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping,
21+
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider)
2022
{
2123
ArgumentGuard.NotNull(controllerResourceMapping);
22-
24+
_nonPrimaryDocumentTypeFactory = new NonPrimaryDocumentTypeFactory(resourceFieldValidationMetadataProvider);
2325
_controllerResourceMapping = controllerResourceMapping;
2426
}
2527

@@ -85,12 +87,12 @@ private static PrimaryRequestMetadata GetPatchRequestMetadata(Type resourceClrTy
8587
return new PrimaryRequestMetadata(documentType);
8688
}
8789

88-
private static RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable<RelationshipAttribute> relationships, bool ignoreHasOneRelationships)
90+
private RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable<RelationshipAttribute> relationships, bool ignoreHasOneRelationships)
8991
{
9092
IEnumerable<RelationshipAttribute> relationshipsOfEndpoint = ignoreHasOneRelationships ? relationships.OfType<HasManyAttribute>() : relationships;
9193

9294
IDictionary<string, Type> requestDocumentTypesByRelationshipName = relationshipsOfEndpoint.ToDictionary(relationship => relationship.PublicName,
93-
NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipRequest);
95+
_nonPrimaryDocumentTypeFactory.GetForRelationshipRequest);
9496

9597
return new RelationshipRequestMetadata(requestDocumentTypesByRelationshipName);
9698
}
@@ -129,18 +131,18 @@ private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type resourceC
129131
return new PrimaryResponseMetadata(documentType);
130132
}
131133

132-
private static SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable<RelationshipAttribute> relationships)
134+
private SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable<RelationshipAttribute> relationships)
133135
{
134136
IDictionary<string, Type> responseDocumentTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName,
135-
NonPrimaryDocumentTypeFactory.Instance.GetForSecondaryResponse);
137+
_nonPrimaryDocumentTypeFactory.GetForSecondaryResponse);
136138

137139
return new SecondaryResponseMetadata(responseDocumentTypesByRelationshipName);
138140
}
139141

140-
private static RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable<RelationshipAttribute> relationships)
142+
private RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable<RelationshipAttribute> relationships)
141143
{
142144
IDictionary<string, Type> responseDocumentTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName,
143-
NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipResponse);
145+
_nonPrimaryDocumentTypeFactory.GetForRelationshipResponse);
144146

145147
return new RelationshipResponseMetadata(responseDocumentTypesByRelationshipName);
146148
}

src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NonPrimaryDocumentTypeFactory.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ internal sealed class NonPrimaryDocumentTypeFactory
1515
private static readonly DocumentOpenTypes RelationshipResponseDocumentOpenTypes = new(typeof(ResourceIdentifierCollectionResponseDocument<>),
1616
typeof(NullableResourceIdentifierResponseDocument<>), typeof(ResourceIdentifierResponseDocument<>));
1717

18-
public static NonPrimaryDocumentTypeFactory Instance { get; } = new();
18+
private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider;
1919

20-
private NonPrimaryDocumentTypeFactory()
20+
public NonPrimaryDocumentTypeFactory(ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider)
2121
{
22+
ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider);
23+
24+
_resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider;
2225
}
2326

2427
public Type GetForSecondaryResponse(RelationshipAttribute relationship)
@@ -42,13 +45,13 @@ public Type GetForRelationshipResponse(RelationshipAttribute relationship)
4245
return Get(relationship, RelationshipResponseDocumentOpenTypes);
4346
}
4447

45-
private static Type Get(RelationshipAttribute relationship, DocumentOpenTypes types)
48+
private Type Get(RelationshipAttribute relationship, DocumentOpenTypes types)
4649
{
4750
// @formatter:nested_ternary_style expanded
4851

4952
Type documentOpenType = relationship is HasManyAttribute
5053
? types.ManyDataOpenType
51-
: relationship.IsNullable()
54+
: _resourceFieldValidationMetadataProvider.IsNullable(relationship)
5255
? types.NullableSingleDataOpenType
5356
: types.SingleDataOpenType;
5457

src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipTypeFactory.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects;
55

66
internal sealed class RelationshipTypeFactory
77
{
8-
public static RelationshipTypeFactory Instance { get; } = new();
8+
private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider;
9+
private readonly NonPrimaryDocumentTypeFactory _nonPrimaryDocumentTypeFactory;
910

10-
private RelationshipTypeFactory()
11+
public RelationshipTypeFactory(ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider)
1112
{
13+
_nonPrimaryDocumentTypeFactory = new NonPrimaryDocumentTypeFactory(resourceFieldValidationMetadataProvider);
14+
_resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider;
1215
}
1316

1417
public Type GetForRequest(RelationshipAttribute relationship)
1518
{
1619
ArgumentGuard.NotNull(relationship);
1720

18-
return NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipRequest(relationship);
21+
return _nonPrimaryDocumentTypeFactory.GetForRelationshipRequest(relationship);
1922
}
2023

2124
public Type GetForResponse(RelationshipAttribute relationship)
@@ -26,7 +29,7 @@ public Type GetForResponse(RelationshipAttribute relationship)
2629

2730
Type relationshipDataOpenType = relationship is HasManyAttribute
2831
? typeof(ToManyRelationshipInResponse<>)
29-
: relationship.IsNullable()
32+
: _resourceFieldValidationMetadataProvider.IsNullable(relationship)
3033
? typeof(NullableToOneRelationshipInResponse<>)
3134
: typeof(ToOneRelationshipInResponse<>);
3235

0 commit comments

Comments
 (0)