diff --git a/appveyor.yml b/appveyor.yml index 433d391b16..bbd35ed160 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,6 +14,7 @@ environment: branches: only: + - openapi-required-and-nullable-properties # TODO: remove - master - openapi - develop diff --git a/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs index a994d78a0e..18c90fdfd2 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs @@ -5,14 +5,19 @@ namespace JsonApiDotNetCore.OpenApi.Client; public interface IJsonApiClient { /// - /// Ensures correct serialization of attributes in a POST/PATCH Resource request body. In JSON:API, an omitted attribute indicates to ignore it, while an - /// attribute that is set to "null" means to clear it. This poses a problem because the serializer cannot distinguish between "you have explicitly set - /// this .NET property to null" vs "you didn't touch it, so it is null by default" when converting an instance to JSON. Therefore, calling this method - /// treats all attributes that contain their default value (null for reference types, 0 for integers, false for booleans, etc) as - /// omitted unless explicitly listed to include them using . + /// + /// Calling this method ensures that attributes containing a default value (null for reference types, 0 for integers, false for + /// booleans, etc) are omitted during serialization, except for those explicitly marked for inclusion in + /// . + /// + /// + /// This is sometimes required to ensure correct serialization of attributes during a POST/PATCH request. In JSON:API, an omitted attribute indicates to + /// ignore it, while an attribute that is set to "null" means to clear it. This poses a problem because the serializer cannot distinguish between "you + /// have explicitly set this .NET property to null" vs "you didn't touch it, so it is null by default" when converting an instance to JSON. + /// /// /// - /// The request document instance for which this registration applies. + /// The request document instance for which default values should be omitted. /// /// /// Optional. A list of expressions to indicate which properties to unconditionally include in the JSON request body. For example: @@ -30,7 +35,7 @@ public interface IJsonApiClient /// An to clear the current registration. For efficient memory usage, it is recommended to wrap calls to this method in a /// using statement, so the registrations are cleaned up after executing the request. /// - IDisposable RegisterAttributesForRequestDocument(TRequestDocument requestDocument, + IDisposable OmitDefaultValuesForAttributesInRequestDocument(TRequestDocument requestDocument, params Expression>[] alwaysIncludedAttributeSelectors) where TRequestDocument : class; } diff --git a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs index 219ac04353..f6d9dbdb2d 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs @@ -3,6 +3,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using SysNotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute; namespace JsonApiDotNetCore.OpenApi.Client; @@ -23,7 +24,7 @@ protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings settings) } /// - public IDisposable RegisterAttributesForRequestDocument(TRequestDocument requestDocument, + public IDisposable OmitDefaultValuesForAttributesInRequestDocument(TRequestDocument requestDocument, params Expression>[] alwaysIncludedAttributeSelectors) where TRequestDocument : class { @@ -43,9 +44,10 @@ public IDisposable RegisterAttributesForRequestDocument _alwaysIncludedAttributesPerRequestDocumentInstance = new(); - private readonly Dictionary> _requestDocumentInstancesPerRequestDocumentType = new(); - private bool _isSerializing; + private readonly Dictionary _attributesObjectInfoByRequestDocument = new(); + private readonly Dictionary> _requestDocumentsByType = new(); + private SerializationScope? _serializationScope; public override bool CanRead => false; - public void RegisterRequestDocument(object requestDocument, AttributeNamesContainer attributes) + public void RegisterRequestDocumentForAttributesOmission(object requestDocument, AttributesObjectInfo attributesObjectInfo) { - _alwaysIncludedAttributesPerRequestDocumentInstance[requestDocument] = attributes; + _attributesObjectInfoByRequestDocument[requestDocument] = attributesObjectInfo; Type requestDocumentType = requestDocument.GetType(); - if (!_requestDocumentInstancesPerRequestDocumentType.ContainsKey(requestDocumentType)) + if (!_requestDocumentsByType.ContainsKey(requestDocumentType)) { - _requestDocumentInstancesPerRequestDocumentType[requestDocumentType] = new HashSet(); + _requestDocumentsByType[requestDocumentType] = new HashSet(); } - _requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Add(requestDocument); + _requestDocumentsByType[requestDocumentType].Add(requestDocument); } - public void RemoveAttributeRegistration(object requestDocument) + public void RemoveRegistration(object requestDocument) { - if (_alwaysIncludedAttributesPerRequestDocumentInstance.ContainsKey(requestDocument)) + if (_attributesObjectInfoByRequestDocument.ContainsKey(requestDocument)) { - _alwaysIncludedAttributesPerRequestDocumentInstance.Remove(requestDocument); + _attributesObjectInfoByRequestDocument.Remove(requestDocument); Type requestDocumentType = requestDocument.GetType(); - _requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Remove(requestDocument); + _requestDocumentsByType[requestDocumentType].Remove(requestDocument); - if (!_requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Any()) + if (!_requestDocumentsByType[requestDocumentType].Any()) { - _requestDocumentInstancesPerRequestDocumentType.Remove(requestDocumentType); + _requestDocumentsByType.Remove(requestDocumentType); } } } @@ -107,71 +109,195 @@ public override bool CanConvert(Type objectType) { ArgumentGuard.NotNull(objectType); - return !_isSerializing && _requestDocumentInstancesPerRequestDocumentType.ContainsKey(objectType); + if (_serializationScope == null) + { + return _requestDocumentsByType.ContainsKey(objectType); + } + + return _serializationScope.ShouldConvertAsAttributesObject(objectType); } public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { - throw new Exception("This code should not be reachable."); + throw new UnreachableCodeException(); } public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { ArgumentGuard.NotNull(writer); + ArgumentGuard.NotNull(value); ArgumentGuard.NotNull(serializer); - if (value != null) + if (_serializationScope == null) { - if (_alwaysIncludedAttributesPerRequestDocumentInstance.ContainsKey(value)) - { - AttributeNamesContainer attributeNamesContainer = _alwaysIncludedAttributesPerRequestDocumentInstance[value]; - serializer.ContractResolver = new JsonApiDocumentContractResolver(attributeNamesContainer); - } + AssertObjectIsRequestDocument(value); - try - { - _isSerializing = true; - serializer.Serialize(writer, value); - } - finally + SerializeRequestDocument(writer, value, serializer); + } + else + { + AttributesObjectInfo? attributesObjectInfo = _serializationScope.AttributesObjectInScope; + + AssertObjectMatchesSerializationScope(attributesObjectInfo, value); + + SerializeAttributesObject(attributesObjectInfo, writer, value, serializer); + } + } + + private void AssertObjectIsRequestDocument(object value) + { + Type objectType = value.GetType(); + + if (!_requestDocumentsByType.ContainsKey(objectType)) + { + throw new UnreachableCodeException(); + } + } + + private void SerializeRequestDocument(JsonWriter writer, object value, JsonSerializer serializer) + { + _serializationScope = new SerializationScope(); + + if (_attributesObjectInfoByRequestDocument.TryGetValue(value, out AttributesObjectInfo? attributesObjectInfo)) + { + _serializationScope.AttributesObjectInScope = attributesObjectInfo; + } + + try + { + serializer.Serialize(writer, value); + } + finally + { + _serializationScope = null; + } + } + + private static void AssertObjectMatchesSerializationScope([SysNotNull] AttributesObjectInfo? attributesObjectInfo, object value) + { + Type objectType = value.GetType(); + + if (attributesObjectInfo == null || !attributesObjectInfo.MatchesType(objectType)) + { + throw new UnreachableCodeException(); + } + } + + private static void SerializeAttributesObject(AttributesObjectInfo alwaysIncludedAttributes, JsonWriter writer, object value, JsonSerializer serializer) + { + AssertRequiredPropertiesAreNotExcluded(value, alwaysIncludedAttributes, writer); + + serializer.ContractResolver = new JsonApiAttributeContractResolver(alwaysIncludedAttributes); + serializer.Serialize(writer, value); + } + + private static void AssertRequiredPropertiesAreNotExcluded(object value, AttributesObjectInfo alwaysIncludedAttributes, JsonWriter jsonWriter) + { + PropertyInfo[] propertyInfos = value.GetType().GetProperties(); + + foreach (PropertyInfo attributesPropertyInfo in propertyInfos) + { + bool isExplicitlyIncluded = alwaysIncludedAttributes.IsAttributeMarkedForInclusion(attributesPropertyInfo.Name); + + if (isExplicitlyIncluded) { - _isSerializing = false; + return; } + + AssertRequiredPropertyIsNotIgnored(value, attributesPropertyInfo, jsonWriter.Path); + } + } + + private static void AssertRequiredPropertyIsNotIgnored(object value, PropertyInfo attribute, string path) + { + JsonPropertyAttribute jsonPropertyForAttribute = attribute.GetCustomAttributes().Single(); + + if (jsonPropertyForAttribute.Required != Required.Always) + { + return; + } + + bool isPropertyIgnored = DefaultValueEqualsCurrentValue(attribute, value); + + if (isPropertyIgnored) + { + throw new JsonSerializationException( + $"Ignored property '{jsonPropertyForAttribute.PropertyName}' must have a value because it is required. Path '{path}'."); } } + + private static bool DefaultValueEqualsCurrentValue(PropertyInfo propertyInfo, object instance) + { + object? currentValue = propertyInfo.GetValue(instance); + object? defaultValue = GetDefaultValue(propertyInfo.PropertyType); + + if (defaultValue == null) + { + return currentValue == null; + } + + return defaultValue.Equals(currentValue); + } + + private static object? GetDefaultValue(Type type) + { + return type.IsValueType ? Activator.CreateInstance(type) : null; + } + } + + private sealed class SerializationScope + { + private bool _isFirstAttemptToConvertAttributes = true; + public AttributesObjectInfo? AttributesObjectInScope { get; set; } + + public bool ShouldConvertAsAttributesObject(Type type) + { + if (!_isFirstAttemptToConvertAttributes || AttributesObjectInScope == null) + { + return false; + } + + if (!AttributesObjectInScope.MatchesType(type)) + { + return false; + } + + _isFirstAttemptToConvertAttributes = false; + return true; + } } - private sealed class AttributeNamesContainer + private sealed class AttributesObjectInfo { - private readonly ISet _attributeNames; - private readonly Type _containerType; + private readonly ISet _attributesMarkedForInclusion; + private readonly Type _attributesObjectType; - public AttributeNamesContainer(ISet attributeNames, Type containerType) + public AttributesObjectInfo(ISet attributesMarkedForInclusion, Type attributesObjectType) { - ArgumentGuard.NotNull(attributeNames); - ArgumentGuard.NotNull(containerType); + ArgumentGuard.NotNull(attributesMarkedForInclusion); + ArgumentGuard.NotNull(attributesObjectType); - _attributeNames = attributeNames; - _containerType = containerType; + _attributesMarkedForInclusion = attributesMarkedForInclusion; + _attributesObjectType = attributesObjectType; } - public bool ContainsAttribute(string name) + public bool IsAttributeMarkedForInclusion(string name) { - return _attributeNames.Contains(name); + return _attributesMarkedForInclusion.Contains(name); } - public bool ContainerMatchesType(Type type) + public bool MatchesType(Type type) { - return _containerType == type; + return _attributesObjectType == type; } } - private sealed class AttributesRegistrationScope : IDisposable + private sealed class RequestDocumentRegistrationScope : IDisposable { private readonly JsonApiJsonConverter _jsonApiJsonConverter; private readonly object _requestDocument; - public AttributesRegistrationScope(JsonApiJsonConverter jsonApiJsonConverter, object requestDocument) + public RequestDocumentRegistrationScope(JsonApiJsonConverter jsonApiJsonConverter, object requestDocument) { ArgumentGuard.NotNull(jsonApiJsonConverter); ArgumentGuard.NotNull(requestDocument); @@ -182,28 +308,28 @@ public AttributesRegistrationScope(JsonApiJsonConverter jsonApiJsonConverter, ob public void Dispose() { - _jsonApiJsonConverter.RemoveAttributeRegistration(_requestDocument); + _jsonApiJsonConverter.RemoveRegistration(_requestDocument); } } - private sealed class JsonApiDocumentContractResolver : DefaultContractResolver + private sealed class JsonApiAttributeContractResolver : DefaultContractResolver { - private readonly AttributeNamesContainer _attributeNamesContainer; + private readonly AttributesObjectInfo _attributesObjectInfo; - public JsonApiDocumentContractResolver(AttributeNamesContainer attributeNamesContainer) + public JsonApiAttributeContractResolver(AttributesObjectInfo attributesObjectInfo) { - ArgumentGuard.NotNull(attributeNamesContainer); + ArgumentGuard.NotNull(attributesObjectInfo); - _attributeNamesContainer = attributeNamesContainer; + _attributesObjectInfo = attributesObjectInfo; } protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { JsonProperty property = base.CreateProperty(member, memberSerialization); - if (_attributeNamesContainer.ContainerMatchesType(property.DeclaringType!)) + if (_attributesObjectInfo.MatchesType(property.DeclaringType!)) { - if (_attributeNamesContainer.ContainsAttribute(property.UnderlyingName!)) + if (_attributesObjectInfo.IsAttributeMarkedForInclusion(property.UnderlyingName!)) { property.NullValueHandling = NullValueHandling.Include; property.DefaultValueHandling = DefaultValueHandling.Include; diff --git a/src/JsonApiDotNetCore.OpenApi.Client/UnreachableCodeException.cs b/src/JsonApiDotNetCore.OpenApi.Client/UnreachableCodeException.cs new file mode 100644 index 0000000000..f1821329d0 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client/UnreachableCodeException.cs @@ -0,0 +1,9 @@ +namespace JsonApiDotNetCore.OpenApi.Client; + +internal sealed class UnreachableCodeException : Exception +{ + public UnreachableCodeException() + : base("This code should not be reachable.") + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs index e4abf42549..7c9cb75a15 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs @@ -112,20 +112,28 @@ private void ExposeSchema(OpenApiReference openApiReference, Type typeRepresente private bool IsFieldRequired(ResourceFieldAttribute field) { - if (field is HasManyAttribute || _resourceTypeInfo.ResourceObjectOpenType != typeof(ResourceObjectInPostRequest<>)) + if (_resourceTypeInfo.ResourceObjectOpenType != typeof(ResourceObjectInPostRequest<>)) { return false; } - bool hasRequiredAttribute = field.Property.HasAttribute(); + if (field.Property.HasAttribute()) + { + return true; + } + + if (field is HasManyAttribute) + { + return false; + } NullabilityInfoContext nullabilityContext = new(); NullabilityInfo nullabilityInfo = nullabilityContext.Create(field.Property); return field.Property.PropertyType.IsValueType switch { - true => hasRequiredAttribute, - false => _options.ValidateModelState ? nullabilityInfo.ReadState == NullabilityState.NotNull || hasRequiredAttribute : hasRequiredAttribute + true => false, + false => _options.ValidateModelState && nullabilityInfo.ReadState == NullabilityState.NotNull }; } diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/RequestDocumentRegistrationLifetimeTests.cs similarity index 78% rename from test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs rename to test/OpenApiClientTests/LegacyClient/RequestDocumentRegistrationLifetimeTests.cs index 4582f9578d..55e882fedf 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestDocumentRegistrationLifetimeTests.cs @@ -6,10 +6,10 @@ namespace OpenApiClientTests.LegacyClient; -public sealed class ClientAttributeRegistrationLifetimeTests +public sealed class RequestDocumentRegistrationLifetimeTests { [Fact] - public async Task Disposed_attribute_registration_for_document_does_not_affect_request() + public async Task Disposed_request_document_registration_does_not_affect_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -27,7 +27,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, airplane => airplane.AirtimeInHours)) { _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); @@ -51,7 +51,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r } [Fact] - public async Task Attribute_registration_can_be_used_for_multiple_requests() + public async Task Request_document_registration_can_be_used_for_multiple_requests() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -72,7 +72,7 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, airplane => airplane.AirtimeInHours)) { _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); @@ -98,7 +98,7 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() } [Fact] - public async Task Request_is_unaffected_by_attribute_registration_for_different_document_of_same_type() + public async Task Request_is_unaffected_by_request_document_registration_of_different_request_document_of_same_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -128,10 +128,10 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { } @@ -153,7 +153,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ } [Fact] - public async Task Attribute_values_can_be_changed_after_attribute_registration() + public async Task Attribute_values_can_be_changed_after_request_document_registration() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -174,7 +174,7 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, airplane => airplane.IsInMaintenance)) { requestDocument.Data.Attributes.IsInMaintenance = false; @@ -196,7 +196,7 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() } [Fact] - public async Task Attribute_registration_is_unaffected_by_successive_attribute_registration_for_document_of_different_type() + public async Task Request_document_registration_is_unaffected_by_successive_registration_of_request_document_of_different_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -223,10 +223,10 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument1, airplane => airplane.IsInMaintenance)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument2, airplane => airplane.AirtimeInHours)) { // Act @@ -247,7 +247,7 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r } [Fact] - public async Task Attribute_registration_is_unaffected_by_preceding_disposed_attribute_registration_for_different_document_of_same_type() + public async Task Request_document_registration_is_unaffected_by_preceding_disposed_registration_of_different_request_document_of_same_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -265,7 +265,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); @@ -288,7 +288,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { // Act @@ -309,7 +309,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } [Fact] - public async Task Attribute_registration_is_unaffected_by_preceding_disposed_attribute_registration_for_document_of_different_type() + public async Task Request_document_registration_is_unaffected_by_preceding_disposed_registration_of_request_document_of_different_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -320,11 +320,14 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att Data = new AirplaneDataInPostRequest { Type = AirplaneResourceType.Airplanes, - Attributes = new AirplaneAttributesInPostRequest() + Attributes = new AirplaneAttributesInPostRequest + { + Name = "Jay Jay the Jet Plane" + } } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument1)); @@ -347,7 +350,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { // Act @@ -368,7 +371,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } [Fact] - public async Task Attribute_registration_is_unaffected_by_preceding_attribute_registration_for_different_document_of_same_type() + public async Task Request_document_registration_is_unaffected_by_preceding_registration_of_different_request_document_of_same_type() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); @@ -398,10 +401,10 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument1, airplane => airplane.SerialNumber)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument2, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index e03e8f1015..8bb0c3f350 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -152,7 +152,7 @@ public async Task Partial_posting_resource_produces_expected_request() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, airplane => airplane.SerialNumber)) { // Act @@ -203,7 +203,7 @@ public async Task Partial_patching_resource_produces_expected_request() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, airplane => airplane.SerialNumber, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act diff --git a/test/OpenApiClientTests/ObjectExtensions.cs b/test/OpenApiClientTests/ObjectExtensions.cs new file mode 100644 index 0000000000..e3790eaa15 --- /dev/null +++ b/test/OpenApiClientTests/ObjectExtensions.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using JsonApiDotNetCore.OpenApi.Client; + +namespace OpenApiClientTests; + +internal static class ObjectExtensions +{ + public static void SetPropertyToDefaultValue(this object target, string propertyName) + { + ArgumentGuard.NotNull(target); + ArgumentGuard.NotNull(propertyName); + + Type declaringType = target.GetType(); + + PropertyInfo property = declaringType.GetProperties().Single(property => property.Name == propertyName); + object? defaultValue = declaringType.IsValueType ? Activator.CreateInstance(declaringType) : null; + + property.SetValue(target, defaultValue); + } +} diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/AlternativeFormRequestTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/AlternativeFormRequestTests.cs new file mode 100644 index 0000000000..daf1bd38b6 --- /dev/null +++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/AlternativeFormRequestTests.cs @@ -0,0 +1,252 @@ +using System.Net; +using FluentAssertions; +using JsonApiDotNetCore.Middleware; +using Microsoft.Net.Http.Headers; +using OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled; + +/// +/// Should consider if the shape of the two tests here is more favourable over the test with the same name in the RequestTests suite. The drawback of the +/// form here is that the expected json string is less easy to read. However the win is that this form allows us to run the tests in a [Theory]. This is +/// relevant because each of these properties represent unique test cases. In the other test form, it is not clear which properties are tested without. +/// For instance: here in Can_exclude_optional_relationships it is immediately clear that the properties we omit are those in the inline data. +/// +public sealed class AlternativeFormRequestTests +{ + private const string HenHouseUrl = "http://localhost/henHouses"; + + private readonly Dictionary _partials = new() + { + { + nameof(HenHouseRelationshipsInPostRequest.OldestChicken), @"""oldestChicken"": { + ""data"": { + ""type"": ""chickens"", + ""id"": ""1"" + } + }" + }, + { + nameof(HenHouseRelationshipsInPostRequest.FirstChicken), @"""firstChicken"": { + ""data"": { + ""type"": ""chickens"", + ""id"": ""1"" + } + }" + }, + { + nameof(HenHouseRelationshipsInPostRequest.AllChickens), @"""allChickens"": { + ""data"": [ + { + ""type"": ""chickens"", + ""id"": ""1"" + } + ] + }" + }, + + { + nameof(HenHouseRelationshipsInPostRequest.ChickensReadyForLaying), @"""chickensReadyForLaying"": { + ""data"": [ + { + ""type"": ""chickens"", + ""id"": ""1"" + } + ] + }" + } + }; + + [Theory] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.OldestChicken))] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.AllChickens))] + public async Task Can_exclude_optional_relationships(string propertyName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + HenHouseRelationshipsInPostRequest relationshipsObject = new() + { + OldestChicken = new NullableToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + FirstChicken = new ToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + AllChickens = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + }, + ChickensReadyForLaying = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + }; + + relationshipsObject.SetPropertyToDefaultValue(propertyName); + + var requestDocument = new HenHousePostRequestDocument + { + Data = new HenHouseDataInPostRequest + { + Relationships = relationshipsObject + } + }; + + await ApiResponse.TranslateAsync(async () => await apiClient.PostHenHouseAsync(requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(HenHouseUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + string body = GetRelationshipsObjectWithSinglePropertyOmitted(propertyName); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""henHouses"", + ""relationships"": " + body + @" + } +}"); + } + + [Theory] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.FirstChicken))] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.ChickensReadyForLaying))] + public async Task Can_exclude_relationships_that_are_required_for_POST_when_performing_PATCH(string propertyName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var relationshipsObject = new HenHouseRelationshipsInPatchRequest + { + OldestChicken = new NullableToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + FirstChicken = new ToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + AllChickens = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + }, + ChickensReadyForLaying = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + }; + + relationshipsObject.SetPropertyToDefaultValue(propertyName); + + var requestDocument = new HenHousePatchRequestDocument + { + Data = new HenHouseDataInPatchRequest + { + Id = "1", + Type = HenHouseResourceType.HenHouses, + Relationships = relationshipsObject + } + }; + + await ApiResponse.TranslateAsync(async () => await apiClient.PatchHenHouseAsync(1, requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Patch); + wrapper.Request.RequestUri.Should().Be(HenHouseUrl + "/1"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + string serializedRelationshipsObject = GetRelationshipsObjectWithSinglePropertyOmitted(propertyName); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""henHouses"", + ""id"": ""1"", + ""relationships"": " + serializedRelationshipsObject + @" + } +}"); + } + + private string GetRelationshipsObjectWithSinglePropertyOmitted(string excludeProperty) + { + string partial = ""; + + foreach ((string key, string relationshipJsonPartial) in _partials) + { + if (excludeProperty == key) + { + continue; + } + + if (partial.Length > 0) + { + partial += ",\n "; + } + + partial += relationshipJsonPartial; + } + + return @"{ + " + partial + @" + }"; + } +} diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequestTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequestTests.cs new file mode 100644 index 0000000000..1ee3ee0fc3 --- /dev/null +++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequestTests.cs @@ -0,0 +1,873 @@ +using System.Net; +using System.Reflection; +using FluentAssertions; +using FluentAssertions.Specialized; +using JsonApiDotNetCore.Middleware; +using Microsoft.Net.Http.Headers; +using Newtonsoft.Json; +using OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled; + +public sealed class RelationshipRequestTests +{ + private const string ChickenUrl = "http://localhost/chickens"; + private const string HenHouseUrl = "http://localhost/henHouses"; + + [Fact] + public async Task Can_exclude_optional_attributes() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new ChickenPostRequestDocument + { + Data = new ChickenDataInPostRequest + { + Attributes = new ChickenAttributesInPostRequest + { + NameOfCurrentFarm = "Cow and Chicken Farm", + Weight = 30, + HasProducedEggs = true + } + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument)); + } + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(ChickenUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""chickens"", + ""attributes"": { + ""nameOfCurrentFarm"": ""Cow and Chicken Farm"", + ""weight"": 30, + ""hasProducedEggs"": true + } + } +}"); + } + + [Theory] + [InlineData(nameof(ChickenAttributesInResponse.NameOfCurrentFarm), "nameOfCurrentFarm")] + [InlineData(nameof(ChickenAttributesInResponse.Weight), "weight")] + [InlineData(nameof(ChickenAttributesInResponse.HasProducedEggs), "hasProducedEggs")] + public async Task Cannot_exclude_required_attribute_when_performing_POST(string propertyName, string jsonName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var attributesInPostRequest = new ChickenAttributesInPostRequest + { + Name = "Chicken", + NameOfCurrentFarm = "Cow and Chicken Farm", + Age = 10, + Weight = 30, + TimeAtCurrentFarmInDays = 100, + HasProducedEggs = true + }; + + attributesInPostRequest.SetPropertyToDefaultValue(propertyName); + + var requestDocument = new ChickenPostRequestDocument + { + Data = new ChickenDataInPostRequest + { + Attributes = attributesInPostRequest + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be($"Ignored property '{jsonName}' must have a value because it is required. Path 'data.attributes'."); + } + } + + [Fact] + public async Task Can_exclude_attributes_that_are_required_for_POST_when_performing_PATCH() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new ChickenPatchRequestDocument + { + Data = new ChickenDataInPatchRequest + { + Id = "1", + Attributes = new ChickenAttributesInPatchRequest + { + Name = "Chicken", + Age = 10, + TimeAtCurrentFarmInDays = 100 + } + } + }; + + // Act + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PatchChickenAsync(1, requestDocument)); + } + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Patch); + wrapper.Request.RequestUri.Should().Be(ChickenUrl + "/1"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""chickens"", + ""id"": ""1"", + ""attributes"": { + ""name"": ""Chicken"", + ""age"": 10, + ""timeAtCurrentFarmInDays"": 100 + } + } +}"); + } + + [Fact] + public async Task Cannot_exclude_id_when_performing_PATCH() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new ChickenPatchRequestDocument + { + Data = new ChickenDataInPatchRequest + { + Attributes = new ChickenAttributesInPatchRequest + { + Name = "Chicken", + NameOfCurrentFarm = "Cow and Chicken Farm", + Age = 10, + Weight = 30, + TimeAtCurrentFarmInDays = 100, + HasProducedEggs = true + } + } + }; + + // Act + Func action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchChickenAsync(1, requestDocument)); + + // Assert + await action.Should().ThrowAsync(); + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be("Cannot write a null value for property 'id'. Property requires a value. Path 'data'."); + } + + [Fact] + public async Task Can_clear_nullable_attributes() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new ChickenPostRequestDocument + { + Data = new ChickenDataInPostRequest + { + Attributes = new ChickenAttributesInPostRequest + { + Name = null, + TimeAtCurrentFarmInDays = null, + NameOfCurrentFarm = "Cow and Chicken Farm", + Age = 10, + Weight = 30, + HasProducedEggs = true + } + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, + chicken => chicken.Name, chicken => chicken.TimeAtCurrentFarmInDays)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument)); + } + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(ChickenUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""chickens"", + ""attributes"": { + ""name"": null, + ""nameOfCurrentFarm"": ""Cow and Chicken Farm"", + ""age"": 10, + ""weight"": 30, + ""timeAtCurrentFarmInDays"": null, + ""hasProducedEggs"": true + } + } +}"); + } + + [Fact] + public async Task Cannot_clear_required_attribute_when_performing_POST() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new ChickenPostRequestDocument + { + Data = new ChickenDataInPostRequest + { + Attributes = new ChickenAttributesInPostRequest + { + Name = "Chicken", + NameOfCurrentFarm = null, + Age = 10, + Weight = 30, + TimeAtCurrentFarmInDays = 100, + HasProducedEggs = true + } + } + }; + + Func> action; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, + chicken => chicken.NameOfCurrentFarm)) + { + // Act + action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument)); + } + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be("Cannot write a null value for property 'nameOfCurrentFarm'. Property requires a value. Path 'data.attributes'."); + } + + [Fact] + public async Task Can_set_default_value_to_ValueType_attributes() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new ChickenPostRequestDocument + { + Data = new ChickenDataInPostRequest + { + Attributes = new ChickenAttributesInPostRequest + { + Name = "Chicken", + NameOfCurrentFarm = "Cow and Chicken Farm", + TimeAtCurrentFarmInDays = 100 + } + } + }; + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(ChickenUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""chickens"", + ""attributes"": { + ""name"": ""Chicken"", + ""nameOfCurrentFarm"": ""Cow and Chicken Farm"", + ""age"": 0, + ""weight"": 0, + ""timeAtCurrentFarmInDays"": 100, + ""hasProducedEggs"": false + } + } +}"); + } + + [Fact] + public async Task Can_exclude_optional_relationships() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new HenHousePostRequestDocument + { + Data = new HenHouseDataInPostRequest + { + Relationships = new HenHouseRelationshipsInPostRequest + { + FirstChicken = new ToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + ChickensReadyForLaying = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + } + } + }; + + await ApiResponse.TranslateAsync(async () => await apiClient.PostHenHouseAsync(requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(HenHouseUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""henHouses"", + ""relationships"": { + ""firstChicken"": { + ""data"": { + ""type"": ""chickens"", + ""id"": ""1"" + } + }, + ""chickensReadyForLaying"": { + ""data"": [ + { + ""type"": ""chickens"", + ""id"": ""1"" + } + ] + } + } + } +}"); + } + + [Theory] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.FirstChicken), "firstChicken")] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.ChickensReadyForLaying), "chickensReadyForLaying")] + public async Task Cannot_exclude_required_relationship_when_performing_POST_with_document_registration(string propertyName, string jsonName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + HenHouseRelationshipsInPostRequest relationshipsInPostDocument = new() + { + OldestChicken = new NullableToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + FirstChicken = new ToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + AllChickens = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + }, + ChickensReadyForLaying = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + }; + + relationshipsInPostDocument.SetPropertyToDefaultValue(propertyName); + + var requestDocument = new HenHousePostRequestDocument + { + Data = new HenHouseDataInPostRequest + { + Relationships = relationshipsInPostDocument + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostHenHouseAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be($"Ignored property '{jsonName}' must have a value because it is required. Path 'data.relationships'."); + } + } + + [Theory] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.FirstChicken), "firstChicken")] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.ChickensReadyForLaying), "chickensReadyForLaying")] + public async Task Cannot_exclude_required_relationship_when_performing_POST_without_document_registration(string propertyName, string jsonName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + HenHouseRelationshipsInPostRequest relationshipsInPostDocument = new() + { + OldestChicken = new NullableToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + FirstChicken = new ToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + AllChickens = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + }, + ChickensReadyForLaying = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + }; + + relationshipsInPostDocument.SetPropertyToDefaultValue(propertyName); + + var requestDocument = new HenHousePostRequestDocument + { + Data = new HenHouseDataInPostRequest + { + Relationships = relationshipsInPostDocument + } + }; + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostHenHouseAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be($"Cannot write a null value for property '{jsonName}'. Property requires a value. Path 'data.relationships'."); + } + + [Fact] + public async Task Can_exclude_relationships_that_are_required_for_POST_when_performing_PATCH() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new HenHousePatchRequestDocument + { + Data = new HenHouseDataInPatchRequest + { + Id = "1", + Type = HenHouseResourceType.HenHouses, + Relationships = new HenHouseRelationshipsInPatchRequest + { + OldestChicken = new NullableToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + AllChickens = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + } + } + }; + + await ApiResponse.TranslateAsync(async () => await apiClient.PatchHenHouseAsync(1, requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Patch); + wrapper.Request.RequestUri.Should().Be(HenHouseUrl + "/1"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""henHouses"", + ""id"": ""1"", + ""relationships"": { + ""oldestChicken"": { + ""data"": { + ""type"": ""chickens"", + ""id"": ""1"" + } + }, + ""allChickens"": { + ""data"": [ + { + ""type"": ""chickens"", + ""id"": ""1"" + } + ] + } + } + } +}"); + } + + [Fact] + public async Task Can_clear_nullable_relationship() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + var requestDocument = new HenHousePostRequestDocument + { + Data = new HenHouseDataInPostRequest + { + Relationships = new HenHouseRelationshipsInPostRequest + { + OldestChicken = new NullableToOneChickenInRequest + { + Data = null + }, + FirstChicken = new ToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + AllChickens = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + }, + ChickensReadyForLaying = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + } + } + }; + + await ApiResponse.TranslateAsync(async () => await apiClient.PostHenHouseAsync(requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(HenHouseUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""henHouses"", + ""relationships"": { + ""oldestChicken"": { + ""data"": null + }, + ""firstChicken"": { + ""data"": { + ""type"": ""chickens"", + ""id"": ""1"" + } + }, + ""allChickens"": { + ""data"": [ + { + ""type"": ""chickens"", + ""id"": ""1"" + } + ] + }, + ""chickensReadyForLaying"": { + ""data"": [ + { + ""type"": ""chickens"", + ""id"": ""1"" + } + ] + } + } + } +}"); + } + + [Theory] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.FirstChicken), "firstChicken")] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.AllChickens), "allChickens")] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.ChickensReadyForLaying), "chickensReadyForLaying")] + public async Task Cannot_clear_non_nullable_relationships_with_document_registration(string propertyName, string jsonName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + HenHouseRelationshipsInPostRequest relationshipsInPostDocument = new() + { + OldestChicken = new NullableToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + FirstChicken = new ToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + AllChickens = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + }, + ChickensReadyForLaying = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + }; + + PropertyInfo relationshipToClearPropertyInfo = relationshipsInPostDocument.GetType().GetProperties().Single(property => property.Name == propertyName); + object relationshipToClear = relationshipToClearPropertyInfo.GetValue(relationshipsInPostDocument)!; + relationshipToClear.SetPropertyToDefaultValue("Data"); + + var requestDocument = new HenHousePostRequestDocument + { + Data = new HenHouseDataInPostRequest + { + Relationships = relationshipsInPostDocument + } + }; + + Func> action; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, + model => model.FirstChicken, model => model.AllChickens, model => model.ChickensReadyForLaying)) + { + // Act + action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PostHenHouseAsync(requestDocument)); + } + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be($"Cannot write a null value for property 'data'. Property requires a value. Path 'data.relationships.{jsonName}'."); + } + + [Theory] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.FirstChicken), "firstChicken")] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.AllChickens), "allChickens")] + [InlineData(nameof(HenHouseRelationshipsInPostRequest.ChickensReadyForLaying), "chickensReadyForLaying")] + public async Task Cannot_clear_non_nullable_relationships_without_document_registration(string propertyName, string jsonName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); + + HenHouseRelationshipsInPostRequest relationshipsInPostDocument = new() + { + OldestChicken = new NullableToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + FirstChicken = new ToOneChickenInRequest + { + Data = new ChickenIdentifier + { + Id = "1", + Type = ChickenResourceType.Chickens + } + }, + AllChickens = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + }, + ChickensReadyForLaying = new ToManyChickenInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = ChickenResourceType.Chickens + } + } + } + }; + + PropertyInfo relationshipToClearPropertyInfo = relationshipsInPostDocument.GetType().GetProperties().Single(property => property.Name == propertyName); + object relationshipToClear = relationshipToClearPropertyInfo.GetValue(relationshipsInPostDocument)!; + relationshipToClear.SetPropertyToDefaultValue("Data"); + + var requestDocument = new HenHousePostRequestDocument + { + Data = new HenHouseDataInPostRequest + { + Relationships = relationshipsInPostDocument + } + }; + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostHenHouseAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be($"Cannot write a null value for property 'data'. Property requires a value. Path 'data.relationships.{jsonName}'."); + } +} diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequiredAttributesTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequiredAttributesTests.cs deleted file mode 100644 index b97535f908..0000000000 --- a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/RequiredAttributesTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Net; -using FluentAssertions; -using FluentAssertions.Specialized; -using JsonApiDotNetCore.Middleware; -using Microsoft.Net.Http.Headers; -using Newtonsoft.Json; -using OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode; -using TestBuildingBlocks; -using Xunit; - -namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled; - -public sealed class RequiredAttributesTests -{ - private const string HostPrefix = "http://localhost/"; - - [Fact] - public async Task Partial_posting_resource_with_explicitly_omitting_required_fields_produces_expected_request() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); - - var requestDocument = new ChickenPostRequestDocument - { - Data = new ChickenDataInPostRequest - { - Attributes = new ChickenAttributesInPostRequest - { - HasProducedEggs = true - } - } - }; - - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, - chicken => chicken.HasProducedEggs)) - { - // Act - await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument)); - } - - // Assert - wrapper.Request.ShouldNotBeNull(); - wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); - wrapper.Request.Method.Should().Be(HttpMethod.Post); - wrapper.Request.RequestUri.Should().Be(HostPrefix + "chickens"); - wrapper.Request.Content.Should().NotBeNull(); - wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); - wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); - - wrapper.RequestBody.Should().BeJson(@"{ - ""data"": { - ""type"": ""chickens"", - ""attributes"": { - ""hasProducedEggs"": true - } - } -}"); - } - - [Fact] - public async Task Partial_posting_resource_without_explicitly_omitting_required_fields_fails() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); - - var requestDocument = new ChickenPostRequestDocument - { - Data = new ChickenDataInPostRequest - { - Attributes = new ChickenAttributesInPostRequest - { - Weight = 3 - } - } - }; - - // Act - Func> action = async () => - await ApiResponse.TranslateAsync(async () => await apiClient.PostChickenAsync(requestDocument)); - - // Assert - ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); - JsonSerializationException exception = assertion.Subject.Single(); - - exception.Message.Should().Be("Cannot write a null value for property 'nameOfCurrentFarm'. Property requires a value. Path 'data.attributes'."); - } - - [Fact] - public async Task Patching_resource_with_missing_id_fails() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new NullableReferenceTypesDisabledClient(wrapper.HttpClient); - - var requestDocument = new ChickenPatchRequestDocument - { - Data = new ChickenDataInPatchRequest - { - Attributes = new ChickenAttributesInPatchRequest - { - Age = 1 - } - } - }; - - Func action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchChickenAsync(1, requestDocument)); - - // Assert - await action.Should().ThrowAsync(); - } -} diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/swagger.g.json b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/swagger.g.json index d71423c997..8357d61d05 100644 --- a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/swagger.g.json +++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled/swagger.g.json @@ -195,6 +195,925 @@ } } } + }, + "/henHouses": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/henHouseCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/henHouseCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "henHouses" + ], + "operationId": "postHenHouse", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/henHousePostRequestDocument" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/henHousePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/henHouses/{id}": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouse", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/henHousePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouse", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/henHousePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "henHouses" + ], + "operationId": "patchHenHouse", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/henHousePatchRequestDocument" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/henHousePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "henHouses" + ], + "operationId": "deleteHenHouse", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/henHouses/{id}/allChickens": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseAllChickens", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseAllChickens", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenCollectionResponseDocument" + } + } + } + } + } + } + }, + "/henHouses/{id}/relationships/allChickens": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseAllChickensRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseAllChickensRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "henHouses" + ], + "operationId": "postHenHouseAllChickensRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyChickenInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "henHouses" + ], + "operationId": "patchHenHouseAllChickensRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyChickenInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "henHouses" + ], + "operationId": "deleteHenHouseAllChickensRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyChickenInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/henHouses/{id}/chickensReadyForLaying": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseChickensReadyForLaying", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseChickensReadyForLaying", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenCollectionResponseDocument" + } + } + } + } + } + } + }, + "/henHouses/{id}/relationships/chickensReadyForLaying": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseChickensReadyForLayingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseChickensReadyForLayingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "henHouses" + ], + "operationId": "postHenHouseChickensReadyForLayingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyChickenInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "henHouses" + ], + "operationId": "patchHenHouseChickensReadyForLayingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyChickenInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "henHouses" + ], + "operationId": "deleteHenHouseChickensReadyForLayingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyChickenInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/henHouses/{id}/firstChicken": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseFirstChicken", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseFirstChicken", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/henHouses/{id}/relationships/firstChicken": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseFirstChickenRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseFirstChickenRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/chickenIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "henHouses" + ], + "operationId": "patchHenHouseFirstChickenRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneChickenInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/henHouses/{id}/oldestChicken": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseOldestChicken", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableChickenSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseOldestChicken", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableChickenSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/henHouses/{id}/relationships/oldestChicken": { + "get": { + "tags": [ + "henHouses" + ], + "operationId": "getHenHouseOldestChickenRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableChickenIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "henHouses" + ], + "operationId": "headHenHouseOldestChickenRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableChickenIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "henHouses" + ], + "operationId": "patchHenHouseOldestChickenRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableToOneChickenInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } } }, "components": { @@ -288,13 +1207,244 @@ "format": "int32", "nullable": true }, - "hasProducedEggs": { - "type": "boolean" + "hasProducedEggs": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "chickenCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/chickenDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "chickenDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/chickenResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/chickenAttributesInPatchRequest" + } + }, + "additionalProperties": false + }, + "chickenDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/chickenResourceType" + }, + "attributes": { + "$ref": "#/components/schemas/chickenAttributesInPostRequest" + } + }, + "additionalProperties": false + }, + "chickenDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/chickenResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/chickenAttributesInResponse" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "chickenIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/chickenResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "chickenIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/chickenIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + }, + "additionalProperties": false + }, + "chickenIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/chickenIdentifier" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "chickenPatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/chickenDataInPatchRequest" + } + }, + "additionalProperties": false + }, + "chickenPostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/chickenDataInPostRequest" + } + }, + "additionalProperties": false + }, + "chickenPrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/chickenDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "chickenResourceType": { + "enum": [ + "chickens" + ], + "type": "string" + }, + "chickenSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/chickenDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" } }, "additionalProperties": false }, - "chickenCollectionResponseDocument": { + "henHouseCollectionResponseDocument": { "required": [ "data", "links" @@ -304,7 +1454,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/chickenDataInResponse" + "$ref": "#/components/schemas/henHouseDataInResponse" } }, "meta": { @@ -320,7 +1470,7 @@ }, "additionalProperties": false }, - "chickenDataInPatchRequest": { + "henHouseDataInPatchRequest": { "required": [ "id", "type" @@ -328,34 +1478,34 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/chickenResourceType" + "$ref": "#/components/schemas/henHouseResourceType" }, "id": { "minLength": 1, "type": "string" }, - "attributes": { - "$ref": "#/components/schemas/chickenAttributesInPatchRequest" + "relationships": { + "$ref": "#/components/schemas/henHouseRelationshipsInPatchRequest" } }, "additionalProperties": false }, - "chickenDataInPostRequest": { + "henHouseDataInPostRequest": { "required": [ "type" ], "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/chickenResourceType" + "$ref": "#/components/schemas/henHouseResourceType" }, - "attributes": { - "$ref": "#/components/schemas/chickenAttributesInPostRequest" + "relationships": { + "$ref": "#/components/schemas/henHouseRelationshipsInPostRequest" } }, "additionalProperties": false }, - "chickenDataInResponse": { + "henHouseDataInResponse": { "required": [ "id", "links", @@ -364,14 +1514,14 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/chickenResourceType" + "$ref": "#/components/schemas/henHouseResourceType" }, "id": { "minLength": 1, "type": "string" }, - "attributes": { - "$ref": "#/components/schemas/chickenAttributesInResponse" + "relationships": { + "$ref": "#/components/schemas/henHouseRelationshipsInResponse" }, "links": { "$ref": "#/components/schemas/linksInResourceObject" @@ -383,31 +1533,31 @@ }, "additionalProperties": false }, - "chickenPatchRequestDocument": { + "henHousePatchRequestDocument": { "required": [ "data" ], "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/chickenDataInPatchRequest" + "$ref": "#/components/schemas/henHouseDataInPatchRequest" } }, "additionalProperties": false }, - "chickenPostRequestDocument": { + "henHousePostRequestDocument": { "required": [ "data" ], "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/chickenDataInPostRequest" + "$ref": "#/components/schemas/henHouseDataInPostRequest" } }, "additionalProperties": false }, - "chickenPrimaryResponseDocument": { + "henHousePrimaryResponseDocument": { "required": [ "data", "links" @@ -415,7 +1565,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/chickenDataInResponse" + "$ref": "#/components/schemas/henHouseDataInResponse" }, "meta": { "type": "object", @@ -430,9 +1580,67 @@ }, "additionalProperties": false }, - "chickenResourceType": { + "henHouseRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "oldestChicken": { + "$ref": "#/components/schemas/nullableToOneChickenInRequest" + }, + "firstChicken": { + "$ref": "#/components/schemas/toOneChickenInRequest" + }, + "allChickens": { + "$ref": "#/components/schemas/toManyChickenInRequest" + }, + "chickensReadyForLaying": { + "$ref": "#/components/schemas/toManyChickenInRequest" + } + }, + "additionalProperties": false + }, + "henHouseRelationshipsInPostRequest": { + "required": [ + "chickensReadyForLaying", + "firstChicken" + ], + "type": "object", + "properties": { + "oldestChicken": { + "$ref": "#/components/schemas/nullableToOneChickenInRequest" + }, + "firstChicken": { + "$ref": "#/components/schemas/toOneChickenInRequest" + }, + "allChickens": { + "$ref": "#/components/schemas/toManyChickenInRequest" + }, + "chickensReadyForLaying": { + "$ref": "#/components/schemas/toManyChickenInRequest" + } + }, + "additionalProperties": false + }, + "henHouseRelationshipsInResponse": { + "type": "object", + "properties": { + "oldestChicken": { + "$ref": "#/components/schemas/nullableToOneChickenInResponse" + }, + "firstChicken": { + "$ref": "#/components/schemas/toOneChickenInResponse" + }, + "allChickens": { + "$ref": "#/components/schemas/toManyChickenInResponse" + }, + "chickensReadyForLaying": { + "$ref": "#/components/schemas/toManyChickenInResponse" + } + }, + "additionalProperties": false + }, + "henHouseResourceType": { "enum": [ - "chickens" + "henHouses" ], "type": "string" }, @@ -461,6 +1669,24 @@ }, "additionalProperties": false }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, "linksInResourceCollectionDocument": { "required": [ "first", @@ -507,6 +1733,62 @@ }, "additionalProperties": false }, + "linksInResourceIdentifierCollectionDocument": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierDocument": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, "linksInResourceObject": { "required": [ "self" @@ -519,6 +1801,202 @@ } }, "additionalProperties": false + }, + "nullValue": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": { } + }, + "nullable": true + }, + "nullableChickenIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/chickenIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "nullableChickenSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/chickenDataInResponse" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "nullableToOneChickenInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/chickenIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneChickenInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/chickenIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "toManyChickenInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/chickenIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyChickenInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/chickenIdentifier" + } + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "toOneChickenInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/chickenIdentifier" + } + }, + "additionalProperties": false + }, + "toOneChickenInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/chickenIdentifier" + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false } } } diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequestTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequestTests.cs new file mode 100644 index 0000000000..aef305b63d --- /dev/null +++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequestTests.cs @@ -0,0 +1,780 @@ +using System.Net; +using FluentAssertions; +using FluentAssertions.Specialized; +using JsonApiDotNetCore.Middleware; +using Microsoft.Net.Http.Headers; +using Newtonsoft.Json; +using OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled; + +public sealed class RequestTests +{ + private const string CowUrl = "http://localhost/cows"; + private const string CowStableUrl = "http://localhost/cowStables"; + + [Fact] + public async Task Can_exclude_optional_attributes() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var requestDocument = new CowPostRequestDocument + { + Data = new CowDataInPostRequest + { + Attributes = new CowAttributesInPostRequest + { + Name = "Cow", + NameOfCurrentFarm = "Cow and Chicken Farm", + Nickname = "Cow", + Weight = 30, + HasProducedMilk = true + } + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostCowAsync(requestDocument)); + } + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(CowUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""cows"", + ""attributes"": { + ""name"": ""Cow"", + ""nameOfCurrentFarm"": ""Cow and Chicken Farm"", + ""nickname"": ""Cow"", + ""weight"": 30, + ""hasProducedMilk"": true + } + } +}"); + } + + [Theory] + [InlineData(nameof(CowAttributesInResponse.Name), "name")] + [InlineData(nameof(CowAttributesInResponse.NameOfCurrentFarm), "nameOfCurrentFarm")] + [InlineData(nameof(CowAttributesInResponse.Nickname), "nickname")] + [InlineData(nameof(CowAttributesInResponse.Weight), "weight")] + [InlineData(nameof(CowAttributesInResponse.HasProducedMilk), "hasProducedMilk")] + public async Task Cannot_exclude_required_attribute_when_performing_POST(string propertyName, string jsonName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var attributesInPostRequest = new CowAttributesInPostRequest + { + Name = "Cow", + NameOfCurrentFarm = "Cow and Chicken Farm", + NameOfPreviousFarm = "Animal Farm", + Nickname = "Cow", + Age = 10, + Weight = 30, + TimeAtCurrentFarmInDays = 100, + HasProducedMilk = true + }; + + attributesInPostRequest.SetPropertyToDefaultValue(propertyName); + + var requestDocument = new CowPostRequestDocument + { + Data = new CowDataInPostRequest + { + Attributes = attributesInPostRequest + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostCowAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be($"Ignored property '{jsonName}' must have a value because it is required. Path 'data.attributes'."); + } + } + + [Fact] + public async Task Can_exclude_attributes_that_are_required_for_POST_when_performing_PATCH() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var requestDocument = new CowPatchRequestDocument + { + Data = new CowDataInPatchRequest + { + Id = "1", + Attributes = new CowAttributesInPatchRequest + { + NameOfPreviousFarm = "Animal Farm", + Age = 10, + TimeAtCurrentFarmInDays = 100 + } + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchCowAsync(1, requestDocument)); + } + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Patch); + wrapper.Request.RequestUri.Should().Be(CowUrl + "/1"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""cows"", + ""id"": ""1"", + ""attributes"": { + ""nameOfPreviousFarm"": ""Animal Farm"", + ""age"": 10, + ""timeAtCurrentFarmInDays"": 100 + } + } +}"); + } + + [Fact] + public async Task Cannot_exclude_id_when_performing_PATCH() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var requestDocument = new CowPatchRequestDocument + { + Data = new CowDataInPatchRequest + { + Attributes = new CowAttributesInPatchRequest + { + Name = "Cow", + NameOfCurrentFarm = "Cow and Chicken Farm", + NameOfPreviousFarm = "Animal Farm", + Nickname = "Cow", + Age = 10, + Weight = 30, + TimeAtCurrentFarmInDays = 100, + HasProducedMilk = true + } + } + }; + + // Act + Func action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchCowAsync(1, requestDocument)); + + // Assert + await action.Should().ThrowAsync(); + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be("Cannot write a null value for property 'id'. Property requires a value. Path 'data'."); + } + + [Fact] + public async Task Can_clear_nullable_attributes() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var requestDocument = new CowPostRequestDocument + { + Data = new CowDataInPostRequest + { + Attributes = new CowAttributesInPostRequest + { + NameOfPreviousFarm = null, + TimeAtCurrentFarmInDays = null, + Name = "Cow", + NameOfCurrentFarm = "Cow and Chicken Farm", + Nickname = "Cow", + Age = 10, + Weight = 30, + HasProducedMilk = true + } + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument, + cow => cow.NameOfPreviousFarm, cow => cow.TimeAtCurrentFarmInDays)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostCowAsync(requestDocument)); + } + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(CowUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""cows"", + ""attributes"": { + ""name"": ""Cow"", + ""nameOfCurrentFarm"": ""Cow and Chicken Farm"", + ""nameOfPreviousFarm"": null, + ""nickname"": ""Cow"", + ""age"": 10, + ""weight"": 30, + ""timeAtCurrentFarmInDays"": null, + ""hasProducedMilk"": true + } + } +}"); + } + + [Fact] + public async Task Can_set_default_value_to_ValueType_attributes() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var requestDocument = new CowPostRequestDocument + { + Data = new CowDataInPostRequest + { + Attributes = new CowAttributesInPostRequest + { + Name = "Cow", + NameOfCurrentFarm = "Cow and Chicken Farm", + NameOfPreviousFarm = "Animal Farm", + Nickname = "Cow", + TimeAtCurrentFarmInDays = 100 + } + } + }; + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostCowAsync(requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(CowUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""cows"", + ""attributes"": { + ""name"": ""Cow"", + ""nameOfCurrentFarm"": ""Cow and Chicken Farm"", + ""nameOfPreviousFarm"": ""Animal Farm"", + ""nickname"": ""Cow"", + ""age"": 0, + ""weight"": 0, + ""timeAtCurrentFarmInDays"": 100, + ""hasProducedMilk"": false + } + } +}"); + } + + [Fact] + public async Task Can_exclude_optional_relationships() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var requestDocument = new CowStablePostRequestDocument + { + Data = new CowStableDataInPostRequest + { + Relationships = new CowStableRelationshipsInPostRequest + { + OldestCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + FirstCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + FavoriteCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + AllCows = new ToManyCowInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = CowResourceType.Cows + } + } + } + } + } + }; + + await ApiResponse.TranslateAsync(async () => await apiClient.PostCowStableAsync(requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(CowStableUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""cowStables"", + ""relationships"": { + ""oldestCow"": { + ""data"": { + ""type"": ""cows"", + ""id"": ""1"" + } + }, + ""firstCow"": { + ""data"": { + ""type"": ""cows"", + ""id"": ""1"" + } + }, + ""favoriteCow"": { + ""data"": { + ""type"": ""cows"", + ""id"": ""1"" + } + }, + ""allCows"": { + ""data"": [ + { + ""type"": ""cows"", + ""id"": ""1"" + } + ] + } + } + } +}"); + } + + [Theory] + [InlineData(nameof(CowStableRelationshipsInPostRequest.OldestCow), "oldestCow")] + [InlineData(nameof(CowStableRelationshipsInPostRequest.FirstCow), "firstCow")] + [InlineData(nameof(CowStableRelationshipsInPostRequest.FavoriteCow), "favoriteCow")] + [InlineData(nameof(CowStableRelationshipsInPostRequest.AllCows), "allCows")] + public async Task Cannot_exclude_required_relationship_when_performing_POST_with_document_registration(string propertyName, string jsonName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + CowStableRelationshipsInPostRequest relationshipsInPostDocument = new() + { + OldestCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + FirstCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + FavoriteCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + CowsReadyForMilking = new ToManyCowInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = CowResourceType.Cows + } + } + }, + AllCows = new ToManyCowInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = CowResourceType.Cows + } + } + } + }; + + relationshipsInPostDocument.SetPropertyToDefaultValue(propertyName); + + var requestDocument = new CowStablePostRequestDocument + { + Data = new CowStableDataInPostRequest + { + Relationships = relationshipsInPostDocument + } + }; + + using (apiClient.OmitDefaultValuesForAttributesInRequestDocument(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostCowStableAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be($"Ignored property '{jsonName}' must have a value because it is required. Path 'data.relationships'."); + } + } + + [Theory] + [InlineData(nameof(CowStableRelationshipsInPostRequest.OldestCow), "oldestCow")] + [InlineData(nameof(CowStableRelationshipsInPostRequest.FirstCow), "firstCow")] + [InlineData(nameof(CowStableRelationshipsInPostRequest.FavoriteCow), "favoriteCow")] + [InlineData(nameof(CowStableRelationshipsInPostRequest.AllCows), "allCows")] + public async Task Cannot_exclude_required_relationship_when_performing_POST_without_document_registration(string propertyName, string jsonName) + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + CowStableRelationshipsInPostRequest relationshipsInPostDocument = new() + { + OldestCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + FirstCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + AlbinoCow = new NullableToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + FavoriteCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + CowsReadyForMilking = new ToManyCowInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = CowResourceType.Cows + } + } + }, + AllCows = new ToManyCowInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = CowResourceType.Cows + } + } + } + }; + + relationshipsInPostDocument.SetPropertyToDefaultValue(propertyName); + + var requestDocument = new CowStablePostRequestDocument + { + Data = new CowStableDataInPostRequest + { + Relationships = relationshipsInPostDocument + } + }; + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostCowStableAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Be($"Cannot write a null value for property '{jsonName}'. Property requires a value. Path 'data.relationships'."); + } + + [Fact] + public async Task Can_exclude_relationships_that_are_required_for_POST_when_performing_PATCH() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var requestDocument = new CowStablePatchRequestDocument + { + Data = new CowStableDataInPatchRequest + { + Id = "1", + Type = CowStableResourceType.CowStables, + Relationships = new CowStableRelationshipsInPatchRequest + { + AlbinoCow = new NullableToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + CowsReadyForMilking = new ToManyCowInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = CowResourceType.Cows + } + } + } + } + } + }; + + await ApiResponse.TranslateAsync(async () => await apiClient.PatchCowStableAsync(1, requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Patch); + wrapper.Request.RequestUri.Should().Be(CowStableUrl + "/1"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""cowStables"", + ""id"": ""1"", + ""relationships"": { + ""albinoCow"": { + ""data"": { + ""type"": ""cows"", + ""id"": ""1"" + } + }, + ""cowsReadyForMilking"": { + ""data"": [ + { + ""type"": ""cows"", + ""id"": ""1"" + } + ] + } + } + } +}"); + } + + [Fact] + public async Task Can_clear_nullable_relationship() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); + + var requestDocument = new CowStablePostRequestDocument + { + Data = new CowStableDataInPostRequest + { + Relationships = new CowStableRelationshipsInPostRequest + { + AlbinoCow = new NullableToOneCowInRequest + { + Data = null + }, + OldestCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + FirstCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + FavoriteCow = new ToOneCowInRequest + { + Data = new CowIdentifier + { + Id = "1", + Type = CowResourceType.Cows + } + }, + CowsReadyForMilking = new ToManyCowInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = CowResourceType.Cows + } + } + }, + AllCows = new ToManyCowInRequest + { + Data = new List + { + new() + { + Id = "1", + Type = CowResourceType.Cows + } + } + } + } + } + }; + + await ApiResponse.TranslateAsync(async () => await apiClient.PostCowStableAsync(requestDocument)); + + // Assert + wrapper.Request.ShouldNotBeNull(); + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(CowStableUrl); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""cowStables"", + ""relationships"": { + ""oldestCow"": { + ""data"": { + ""type"": ""cows"", + ""id"": ""1"" + } + }, + ""firstCow"": { + ""data"": { + ""type"": ""cows"", + ""id"": ""1"" + } + }, + ""albinoCow"": { + ""data"": null + }, + ""favoriteCow"": { + ""data"": { + ""type"": ""cows"", + ""id"": ""1"" + } + }, + ""cowsReadyForMilking"": { + ""data"": [ + { + ""type"": ""cows"", + ""id"": ""1"" + } + ] + }, + ""allCows"": { + ""data"": [ + { + ""type"": ""cows"", + ""id"": ""1"" + } + ] + } + } + } +}"); + } +} diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequiredAttributesTests.cs b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequiredAttributesTests.cs deleted file mode 100644 index 5f35ec7f29..0000000000 --- a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/RequiredAttributesTests.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Net; -using FluentAssertions; -using FluentAssertions.Specialized; -using JsonApiDotNetCore.Middleware; -using Microsoft.Net.Http.Headers; -using Newtonsoft.Json; -using OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled.GeneratedCode; -using TestBuildingBlocks; -using Xunit; - -namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled; - -public sealed class RequiredAttributesTests -{ - private const string HostPrefix = "http://localhost/"; - - [Fact] - public async Task Partial_posting_resource_with_explicitly_omitting_required_fields_produces_expected_request() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); - - var requestDocument = new CowPostRequestDocument - { - Data = new CowDataInPostRequest - { - Attributes = new CowAttributesInPostRequest - { - HasProducedMilk = true, - Weight = 1100 - } - } - }; - - using (apiClient.RegisterAttributesForRequestDocument(requestDocument)) - { - // Act - await ApiResponse.TranslateAsync(async () => await apiClient.PostCowAsync(requestDocument)); - } - - // Assert - wrapper.Request.ShouldNotBeNull(); - wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); - wrapper.Request.Method.Should().Be(HttpMethod.Post); - wrapper.Request.RequestUri.Should().Be(HostPrefix + "cows"); - wrapper.Request.Content.Should().NotBeNull(); - wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); - wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); - - wrapper.RequestBody.Should().BeJson(@"{ - ""data"": { - ""type"": ""cows"", - ""attributes"": { - ""weight"": 1100, - ""hasProducedMilk"": true - } - } -}"); - } - - [Fact] - public async Task Partial_posting_resource_without_explicitly_omitting_required_fields_produces_expected_request() - { - // Arrange - using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - var apiClient = new NullableReferenceTypesEnabledClient(wrapper.HttpClient); - - var requestDocument = new CowPostRequestDocument - { - Data = new CowDataInPostRequest - { - Attributes = new CowAttributesInPostRequest - { - Weight = 1100, - Age = 5, - Name = "Cow 1", - NameOfCurrentFarm = "123", - NameOfPreviousFarm = "123" - } - } - }; - - // Act - Func> - action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PostCowAsync(requestDocument)); - - // Assert - ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); - JsonSerializationException exception = assertion.Subject.Single(); - - exception.Message.Should().Be("Cannot write a null value for property 'nickname'. Property requires a value. Path 'data.attributes'."); - } -} diff --git a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/swagger.g.json b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/swagger.g.json index 14669b5c84..1f2d189d46 100644 --- a/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/swagger.g.json +++ b/test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled/swagger.g.json @@ -195,6 +195,1227 @@ } } } + }, + "/cowStables": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowStableCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowStableCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "cowStables" + ], + "operationId": "postCowStable", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowStablePostRequestDocument" + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowStablePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/cowStables/{id}": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStable", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowStablePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStable", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowStablePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "cowStables" + ], + "operationId": "patchCowStable", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowStablePatchRequestDocument" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowStablePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "cowStables" + ], + "operationId": "deleteCowStable", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/cowStables/{id}/albinoCow": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableAlbinoCow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableCowSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableAlbinoCow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableCowSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/cowStables/{id}/relationships/albinoCow": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableAlbinoCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableCowIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableAlbinoCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableCowIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "cowStables" + ], + "operationId": "patchCowStableAlbinoCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullableToOneCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/cowStables/{id}/allCows": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableAllCows", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableAllCows", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowCollectionResponseDocument" + } + } + } + } + } + } + }, + "/cowStables/{id}/relationships/allCows": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableAllCowsRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableAllCowsRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "cowStables" + ], + "operationId": "postCowStableAllCowsRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "cowStables" + ], + "operationId": "patchCowStableAllCowsRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "cowStables" + ], + "operationId": "deleteCowStableAllCowsRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/cowStables/{id}/cowsReadyForMilking": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableCowsReadyForMilking", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableCowsReadyForMilking", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowCollectionResponseDocument" + } + } + } + } + } + } + }, + "/cowStables/{id}/relationships/cowsReadyForMilking": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableCowsReadyForMilkingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableCowsReadyForMilkingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "cowStables" + ], + "operationId": "postCowStableCowsReadyForMilkingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "cowStables" + ], + "operationId": "patchCowStableCowsReadyForMilkingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "cowStables" + ], + "operationId": "deleteCowStableCowsReadyForMilkingRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toManyCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/cowStables/{id}/favoriteCow": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableFavoriteCow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableFavoriteCow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/cowStables/{id}/relationships/favoriteCow": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableFavoriteCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableFavoriteCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "cowStables" + ], + "operationId": "patchCowStableFavoriteCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/cowStables/{id}/firstCow": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableFirstCow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableFirstCow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/cowStables/{id}/relationships/firstCow": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableFirstCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableFirstCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "cowStables" + ], + "operationId": "patchCowStableFirstCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/cowStables/{id}/oldestCow": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableOldestCow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableOldestCow", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/cowStables/{id}/relationships/oldestCow": { + "get": { + "tags": [ + "cowStables" + ], + "operationId": "getCowStableOldestCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "cowStables" + ], + "operationId": "headCowStableOldestCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/cowIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "cowStables" + ], + "operationId": "patchCowStableOldestCowRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/toOneCowInRequest" + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } } }, "components": { @@ -317,7 +1538,238 @@ }, "additionalProperties": false }, - "cowCollectionResponseDocument": { + "cowCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/cowDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + }, + "additionalProperties": false + }, + "cowDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/cowResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/cowAttributesInPatchRequest" + } + }, + "additionalProperties": false + }, + "cowDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/cowResourceType" + }, + "attributes": { + "$ref": "#/components/schemas/cowAttributesInPostRequest" + } + }, + "additionalProperties": false + }, + "cowDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/cowResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/cowAttributesInResponse" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "cowIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/cowResourceType" + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "cowIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/cowIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + }, + "additionalProperties": false + }, + "cowIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/cowIdentifier" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "cowPatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/cowDataInPatchRequest" + } + }, + "additionalProperties": false + }, + "cowPostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/cowDataInPostRequest" + } + }, + "additionalProperties": false + }, + "cowPrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/cowDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "cowResourceType": { + "enum": [ + "cows" + ], + "type": "string" + }, + "cowSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/cowDataInResponse" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "cowStableCollectionResponseDocument": { "required": [ "data", "links" @@ -327,7 +1779,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/cowDataInResponse" + "$ref": "#/components/schemas/cowStableDataInResponse" } }, "meta": { @@ -343,7 +1795,7 @@ }, "additionalProperties": false }, - "cowDataInPatchRequest": { + "cowStableDataInPatchRequest": { "required": [ "id", "type" @@ -351,34 +1803,34 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/cowResourceType" + "$ref": "#/components/schemas/cowStableResourceType" }, "id": { "minLength": 1, "type": "string" }, - "attributes": { - "$ref": "#/components/schemas/cowAttributesInPatchRequest" + "relationships": { + "$ref": "#/components/schemas/cowStableRelationshipsInPatchRequest" } }, "additionalProperties": false }, - "cowDataInPostRequest": { + "cowStableDataInPostRequest": { "required": [ "type" ], "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/cowResourceType" + "$ref": "#/components/schemas/cowStableResourceType" }, - "attributes": { - "$ref": "#/components/schemas/cowAttributesInPostRequest" + "relationships": { + "$ref": "#/components/schemas/cowStableRelationshipsInPostRequest" } }, "additionalProperties": false }, - "cowDataInResponse": { + "cowStableDataInResponse": { "required": [ "id", "links", @@ -387,14 +1839,14 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/cowResourceType" + "$ref": "#/components/schemas/cowStableResourceType" }, "id": { "minLength": 1, "type": "string" }, - "attributes": { - "$ref": "#/components/schemas/cowAttributesInResponse" + "relationships": { + "$ref": "#/components/schemas/cowStableRelationshipsInResponse" }, "links": { "$ref": "#/components/schemas/linksInResourceObject" @@ -406,31 +1858,31 @@ }, "additionalProperties": false }, - "cowPatchRequestDocument": { + "cowStablePatchRequestDocument": { "required": [ "data" ], "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/cowDataInPatchRequest" + "$ref": "#/components/schemas/cowStableDataInPatchRequest" } }, "additionalProperties": false }, - "cowPostRequestDocument": { + "cowStablePostRequestDocument": { "required": [ "data" ], "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/cowDataInPostRequest" + "$ref": "#/components/schemas/cowStableDataInPostRequest" } }, "additionalProperties": false }, - "cowPrimaryResponseDocument": { + "cowStablePrimaryResponseDocument": { "required": [ "data", "links" @@ -438,7 +1890,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/cowDataInResponse" + "$ref": "#/components/schemas/cowStableDataInResponse" }, "meta": { "type": "object", @@ -453,9 +1905,87 @@ }, "additionalProperties": false }, - "cowResourceType": { + "cowStableRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "oldestCow": { + "$ref": "#/components/schemas/toOneCowInRequest" + }, + "firstCow": { + "$ref": "#/components/schemas/toOneCowInRequest" + }, + "albinoCow": { + "$ref": "#/components/schemas/nullableToOneCowInRequest" + }, + "favoriteCow": { + "$ref": "#/components/schemas/toOneCowInRequest" + }, + "cowsReadyForMilking": { + "$ref": "#/components/schemas/toManyCowInRequest" + }, + "allCows": { + "$ref": "#/components/schemas/toManyCowInRequest" + } + }, + "additionalProperties": false + }, + "cowStableRelationshipsInPostRequest": { + "required": [ + "allCows", + "favoriteCow", + "firstCow", + "oldestCow" + ], + "type": "object", + "properties": { + "oldestCow": { + "$ref": "#/components/schemas/toOneCowInRequest" + }, + "firstCow": { + "$ref": "#/components/schemas/toOneCowInRequest" + }, + "albinoCow": { + "$ref": "#/components/schemas/nullableToOneCowInRequest" + }, + "favoriteCow": { + "$ref": "#/components/schemas/toOneCowInRequest" + }, + "cowsReadyForMilking": { + "$ref": "#/components/schemas/toManyCowInRequest" + }, + "allCows": { + "$ref": "#/components/schemas/toManyCowInRequest" + } + }, + "additionalProperties": false + }, + "cowStableRelationshipsInResponse": { + "type": "object", + "properties": { + "oldestCow": { + "$ref": "#/components/schemas/toOneCowInResponse" + }, + "firstCow": { + "$ref": "#/components/schemas/toOneCowInResponse" + }, + "albinoCow": { + "$ref": "#/components/schemas/nullableToOneCowInResponse" + }, + "favoriteCow": { + "$ref": "#/components/schemas/toOneCowInResponse" + }, + "cowsReadyForMilking": { + "$ref": "#/components/schemas/toManyCowInResponse" + }, + "allCows": { + "$ref": "#/components/schemas/toManyCowInResponse" + } + }, + "additionalProperties": false + }, + "cowStableResourceType": { "enum": [ - "cows" + "cowStables" ], "type": "string" }, @@ -484,6 +2014,24 @@ }, "additionalProperties": false }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, "linksInResourceCollectionDocument": { "required": [ "first", @@ -530,6 +2078,62 @@ }, "additionalProperties": false }, + "linksInResourceIdentifierCollectionDocument": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierDocument": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, "linksInResourceObject": { "required": [ "self" @@ -542,6 +2146,202 @@ } }, "additionalProperties": false + }, + "nullValue": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": { } + }, + "nullable": true + }, + "nullableCowIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/cowIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + }, + "additionalProperties": false + }, + "nullableCowSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/cowDataInResponse" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + } + }, + "additionalProperties": false + }, + "nullableToOneCowInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/cowIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneCowInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/cowIdentifier" + }, + { + "$ref": "#/components/schemas/nullValue" + } + ] + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "toManyCowInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/cowIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyCowInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/cowIdentifier" + } + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "toOneCowInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/cowIdentifier" + } + }, + "additionalProperties": false + }, + "toOneCowInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/cowIdentifier" + }, + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false } } } diff --git a/test/OpenApiTests/JsonElementExtensions.cs b/test/OpenApiTests/JsonElementExtensions.cs index 6f1ffe1432..3649127917 100644 --- a/test/OpenApiTests/JsonElementExtensions.cs +++ b/test/OpenApiTests/JsonElementExtensions.cs @@ -2,6 +2,7 @@ using BlushingPenguin.JsonPath; using FluentAssertions; using FluentAssertions.Execution; +using JetBrains.Annotations; using TestBuildingBlocks; namespace OpenApiTests; @@ -33,6 +34,14 @@ public static void ShouldBeString(this JsonElement source, string value) } public static SchemaReferenceIdContainer ShouldBeSchemaReferenceId(this JsonElement source, string value) + { + string schemaReferenceId = GetSchemaReferenceId(source); + schemaReferenceId.Should().Be(value); + + return new SchemaReferenceIdContainer(value); + } + + private static string GetSchemaReferenceId(this JsonElement source) { source.ValueKind.Should().Be(JsonValueKind.String); @@ -40,9 +49,14 @@ public static SchemaReferenceIdContainer ShouldBeSchemaReferenceId(this JsonElem jsonElementValue.ShouldNotBeNull(); string schemaReferenceId = jsonElementValue.Split('/').Last(); - schemaReferenceId.Should().Be(value); + return schemaReferenceId; + } - return new SchemaReferenceIdContainer(value); + public static void WithSchemaReferenceId(this JsonElement subject, [InstantHandle] Action continuation) + { + string schemaReferenceId = GetSchemaReferenceId(subject); + + continuation(schemaReferenceId); } public sealed class SchemaReferenceIdContainer diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/HenHouse.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/HenHouse.cs new file mode 100644 index 0000000000..979b029717 --- /dev/null +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/HenHouse.cs @@ -0,0 +1,27 @@ +#nullable disable + +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.SchemaProperties.NullableReferenceTypesDisabled; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.SchemaProperties")] +public sealed class HenHouse : Identifiable +{ + [HasOne] + public Chicken OldestChicken { get; set; } + + [Required] + [HasOne] + public Chicken FirstChicken { get; set; } + + [HasMany] + public ICollection AllChickens { get; set; } = new HashSet(); + + [Required] + [HasMany] + public ICollection ChickensReadyForLaying { get; set; } = new HashSet(); +} diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/ModelStateValidationDisabledTests.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/ModelStateValidationDisabledTests.cs index 0149a07e32..0b88c3f88b 100644 --- a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/ModelStateValidationDisabledTests.cs +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/ModelStateValidationDisabledTests.cs @@ -17,6 +17,7 @@ public ModelStateValidationDisabledTests( _testContext = testContext; testContext.UseController(); + testContext.UseController(); } [Theory] @@ -64,4 +65,48 @@ public async Task Schema_for_attributes_in_PATCH_request_should_have_no_required // Assert document.ShouldNotContainPath("components.schemas.chickenAttributesInPatchRequest.required"); } + + [Theory] + [InlineData("firstChicken")] + [InlineData("chickensReadyForLaying")] + public async Task Property_in_schema_for_relationships_in_POST_request_should_be_required(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.henHouseRelationshipsInPostRequest.required").With(propertySet => + { + var requiredAttributes = JsonSerializer.Deserialize>(propertySet.GetRawText()); + + requiredAttributes.Should().Contain(propertyName); + }); + } + + [Theory] + [InlineData("oldestChicken")] + [InlineData("allChickens")] + public async Task Property_in_schema_for_relationships_in_POST_request_should_not_be_required(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.henHouseRelationshipsInPostRequest.required").With(propertySet => + { + var requiredProperties = JsonSerializer.Deserialize>(propertySet.GetRawText()); + + requiredProperties.Should().NotContain(propertyName); + }); + } + + [Fact] + public async Task Schema_for_relationships_in_PATCH_request_should_have_no_required_properties() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.henHouseRelationshipsInPatchRequest.required"); + } } diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/ModelStateValidationEnabledTests.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/ModelStateValidationEnabledTests.cs index e200fe365c..bd31a1f25a 100644 --- a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/ModelStateValidationEnabledTests.cs +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/ModelStateValidationEnabledTests.cs @@ -16,6 +16,7 @@ public ModelStateValidationEnabledTests( _testContext = testContext; testContext.UseController(); + testContext.UseController(); } [Theory] @@ -63,4 +64,48 @@ public async Task Schema_for_attributes_in_PATCH_request_should_have_no_required // Assert document.ShouldNotContainPath("components.schemas.chickenAttributesInPatchRequest.required"); } + + [Theory] + [InlineData("firstChicken")] + [InlineData("chickensReadyForLaying")] + public async Task Property_in_schema_for_relationships_in_POST_request_should_be_required(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.henHouseRelationshipsInPostRequest.required").With(propertySet => + { + var requiredAttributes = JsonSerializer.Deserialize>(propertySet.GetRawText()); + + requiredAttributes.Should().Contain(propertyName); + }); + } + + [Theory] + [InlineData("oldestChicken")] + [InlineData("allChickens")] + public async Task Property_in_schema_for_relationships_in_POST_request_should_not_be_required(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.henHouseRelationshipsInPostRequest.required").With(propertySet => + { + var requiredProperties = JsonSerializer.Deserialize>(propertySet.GetRawText()); + + requiredProperties.Should().NotContain(propertyName); + }); + } + + [Fact] + public async Task Schema_for_relationships_in_PATCH_request_should_have_no_required_properties() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.henHouseRelationshipsInPatchRequest.required"); + } } diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/NullabilityTests.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/NullabilityTests.cs index a5485de6e5..dca51da02b 100644 --- a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/NullabilityTests.cs +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/NullabilityTests.cs @@ -15,13 +15,14 @@ public NullabilityTests(OpenApiTestContext(); + testContext.UseController(); testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesDisabled"; } [Theory] [InlineData("name")] [InlineData("timeAtCurrentFarmInDays")] - public async Task Property_in_schema_for_resource_should_be_nullable(string propertyName) + public async Task Property_in_schema_for_attribute_of_resource_should_be_nullable(string propertyName) { // Act JsonElement document = await _testContext.GetSwaggerDocumentAsync(); @@ -41,7 +42,7 @@ public async Task Property_in_schema_for_resource_should_be_nullable(string prop [InlineData("age")] [InlineData("weight")] [InlineData("hasProducedEggs")] - public async Task Property_in_schema_for_resource_should_not_be_nullable(string propertyName) + public async Task Property_in_schema_for_attribute_of_should_not_be_nullable(string propertyName) { // Act JsonElement document = await _testContext.GetSwaggerDocumentAsync(); @@ -55,4 +56,40 @@ public async Task Property_in_schema_for_resource_should_not_be_nullable(string }); }); } + + [Theory] + [InlineData("oldestChicken")] + public async Task Property_in_schema_for_relationship_of_resource_should_be_nullable(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.henHouseRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{propertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); + }); + }); + } + + [Theory] + [InlineData("allChickens")] + [InlineData("firstChicken")] + [InlineData("chickensReadyForLaying")] + public async Task Data_property_in_schema_for_relationship_of_resource_should_not_be_nullable(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.henHouseRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{propertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").ShouldNotContainPath("oneOf[1].$ref"); + }); + }); + } } diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/NullableReferenceTypesDisabledDbContext.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/NullableReferenceTypesDisabledDbContext.cs index 16bdc07e15..a93f4bf779 100644 --- a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/NullableReferenceTypesDisabledDbContext.cs +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesDisabled/NullableReferenceTypesDisabledDbContext.cs @@ -4,13 +4,33 @@ namespace OpenApiTests.SchemaProperties.NullableReferenceTypesDisabled; +// @formatter:wrap_chained_method_calls chop_always + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class NullableReferenceTypesDisabledDbContext : TestableDbContext { public DbSet Chicken => Set(); + public DbSet HenHouse => Set(); public NullableReferenceTypesDisabledDbContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(resource => resource.OldestChicken); + + builder.Entity() + .HasOne(resource => resource.FirstChicken); + + builder.Entity() + .HasMany(resource => resource.AllChickens); + + builder.Entity() + .HasMany(resource => resource.ChickensReadyForLaying); + + base.OnModelCreating(builder); + } } diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/CowStable.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/CowStable.cs new file mode 100644 index 0000000000..b267423501 --- /dev/null +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/CowStable.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.SchemaProperties.NullableReferenceTypesEnabled; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(ControllerNamespace = "OpenApiTests.SchemaProperties")] +public sealed class CowStable : Identifiable +{ + [HasOne] + public Cow OldestCow { get; set; } = null!; + + [Required] + [HasOne] + public Cow FirstCow { get; set; } = null!; + + [HasOne] + public Cow? AlbinoCow { get; set; } + + [Required] + [HasOne] + public Cow? FavoriteCow { get; set; } + + [HasMany] + public ICollection CowsReadyForMilking { get; set; } = new HashSet(); + + [Required] + [HasMany] + public ICollection AllCows { get; set; } = new HashSet(); +} diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/ModelStateValidationDisabledTests.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/ModelStateValidationDisabledTests.cs index 988fe06896..fb83c4ea5f 100644 --- a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/ModelStateValidationDisabledTests.cs +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/ModelStateValidationDisabledTests.cs @@ -17,6 +17,7 @@ public ModelStateValidationDisabledTests( _testContext = testContext; testContext.UseController(); + testContext.UseController(); } [Theory] @@ -65,4 +66,50 @@ public async Task Schema_for_attributes_in_PATCH_request_should_have_no_required // Assert document.ShouldNotContainPath("components.schemas.chickenAttributesInPatchRequest.required"); } + + [Theory] + [InlineData("firstCow")] + [InlineData("allCows")] + [InlineData("favoriteCow")] + public async Task Property_in_schema_for_relationships_in_POST_request_should_be_required(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.cowStableRelationshipsInPostRequest.required").With(propertySet => + { + var requiredAttributes = JsonSerializer.Deserialize>(propertySet.GetRawText()); + + requiredAttributes.Should().Contain(propertyName); + }); + } + + [Theory] + [InlineData("oldestCow")] + [InlineData("cowsReadyForMilking")] + [InlineData("albinoCow")] + public async Task Property_in_schema_for_relationships_in_POST_request_should_not_be_required(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.cowStableRelationshipsInPostRequest.required").With(propertySet => + { + var requiredProperties = JsonSerializer.Deserialize>(propertySet.GetRawText()); + + requiredProperties.Should().NotContain(propertyName); + }); + } + + [Fact] + public async Task Schema_for_relationships_in_PATCH_request_should_have_no_required_properties() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.cowStableRelationshipsInPatchRequest.required"); + } } diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/ModelStateValidationEnabledTests.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/ModelStateValidationEnabledTests.cs index 70e4b3f7a4..d45937a580 100644 --- a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/ModelStateValidationEnabledTests.cs +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/ModelStateValidationEnabledTests.cs @@ -16,6 +16,7 @@ public ModelStateValidationEnabledTests( _testContext = testContext; testContext.UseController(); + testContext.UseController(); } [Theory] @@ -64,4 +65,50 @@ public async Task Schema_for_attributes_in_PATCH_request_should_have_no_required // Assert document.ShouldNotContainPath("components.schemas.chickenAttributesInPatchRequest.required"); } + + [Theory] + [InlineData("oldestCow")] + [InlineData("firstCow")] + [InlineData("allCows")] + [InlineData("favoriteCow")] + public async Task Property_in_schema_for_relationships_in_POST_request_should_be_required(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.cowStableRelationshipsInPostRequest.required").With(propertySet => + { + var requiredAttributes = JsonSerializer.Deserialize>(propertySet.GetRawText()); + + requiredAttributes.Should().Contain(propertyName); + }); + } + + [Theory] + [InlineData("cowsReadyForMilking")] + [InlineData("albinoCow")] + public async Task Property_in_schema_for_relationships_in_POST_request_should_not_be_required(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.cowStableRelationshipsInPostRequest.required").With(propertySet => + { + var requiredProperties = JsonSerializer.Deserialize>(propertySet.GetRawText()); + + requiredProperties.Should().NotContain(propertyName); + }); + } + + [Fact] + public async Task Schema_for_relationships_in_PATCH_request_should_have_no_required_properties() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.cowStableRelationshipsInPatchRequest.required"); + } } diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/NullabilityTests.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/NullabilityTests.cs index 3b8d0597d7..5652c61f33 100644 --- a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/NullabilityTests.cs +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/NullabilityTests.cs @@ -15,13 +15,14 @@ public NullabilityTests(OpenApiTestContext(); + testContext.UseController(); testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/SchemaProperties/NullableReferenceTypesEnabled"; } [Theory] [InlineData("nameOfPreviousFarm")] [InlineData("timeAtCurrentFarmInDays")] - public async Task Property_in_schema_for_resource_should_be_nullable(string propertyName) + public async Task Property_in_schema_for_attribute_of_resource_should_be_nullable(string propertyName) { // Act JsonElement document = await _testContext.GetSwaggerDocumentAsync(); @@ -43,7 +44,7 @@ public async Task Property_in_schema_for_resource_should_be_nullable(string prop [InlineData("age")] [InlineData("weight")] [InlineData("hasProducedMilk")] - public async Task Property_in_schema_for_resource_should_not_be_nullable(string attributeName) + public async Task Property_in_schema_for_attribute_of_resource_should_not_be_nullable(string attributeName) { // Act JsonElement document = await _testContext.GetSwaggerDocumentAsync(); @@ -57,4 +58,42 @@ public async Task Property_in_schema_for_resource_should_not_be_nullable(string }); }); } + + [Theory] + [InlineData("albinoCow")] + public async Task Property_in_schema_for_relationship_of_resource_should_be_nullable(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.cowStableRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{propertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); + }); + }); + } + + [Theory] + [InlineData("oldestCow")] + [InlineData("firstCow")] + [InlineData("cowsReadyForMilking")] + [InlineData("allCows")] + [InlineData("favoriteCow")] + public async Task Data_property_in_schema_for_relationship_of_resource_should_not_be_nullable(string propertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.cowStableRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{propertyName}.$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").ShouldNotContainPath("oneOf[1].$ref"); + }); + }); + } } diff --git a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/NullableReferenceTypesEnabledDbContext.cs b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/NullableReferenceTypesEnabledDbContext.cs index b7011e7d27..e83298f28d 100644 --- a/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/NullableReferenceTypesEnabledDbContext.cs +++ b/test/OpenApiTests/SchemaProperties/NullableReferenceTypesEnabled/NullableReferenceTypesEnabledDbContext.cs @@ -4,13 +4,39 @@ namespace OpenApiTests.SchemaProperties.NullableReferenceTypesEnabled; +// @formatter:wrap_chained_method_calls chop_always + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class NullableReferenceTypesEnabledDbContext : TestableDbContext { public DbSet Cow => Set(); + public DbSet CowStable => Set(); public NullableReferenceTypesEnabledDbContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(resource => resource.OldestCow); + + builder.Entity() + .HasOne(resource => resource.FirstCow); + + builder.Entity() + .HasOne(resource => resource.AlbinoCow); + + builder.Entity() + .HasOne(resource => resource.FavoriteCow); + + builder.Entity() + .HasMany(resource => resource.AllCows); + + builder.Entity() + .HasMany(resource => resource.CowsReadyForMilking); + + base.OnModelCreating(builder); + } }