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/ApiException.cs b/src/JsonApiDotNetCore.OpenApi.Client/ApiException.cs index 1224308af9..cc7cdc8104 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/ApiException.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/ApiException.cs @@ -15,7 +15,7 @@ public sealed class ApiException : Exception public IReadOnlyDictionary> Headers { get; } - public ApiException(string message, int statusCode, string? response, IReadOnlyDictionary> headers, Exception innerException) + public ApiException(string message, int statusCode, string? response, IReadOnlyDictionary> headers, Exception? innerException) : base($"{message}\n\nStatus: {statusCode}\nResponse: \n{response ?? "(null)"}", innerException) { StatusCode = statusCode; diff --git a/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs index a994d78a0e..5cb98a7b9a 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 WithPartialAttributeSerialization(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..f119396747 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 WithPartialAttributeSerialization(TRequestDocument requestDocument, params Expression>[] alwaysIncludedAttributeSelectors) where TRequestDocument : class { @@ -39,13 +40,14 @@ public IDisposable RegisterAttributesForRequestDocument article.Title'."); + throw new ArgumentException( + $"The expression '{nameof(alwaysIncludedAttributeSelectors)}' should select a single property. For example: 'article => article.Title'."); } } - _jsonApiJsonConverter.RegisterRequestDocument(requestDocument, new AttributeNamesContainer(attributeNames, typeof(TAttributesObject))); + _jsonApiJsonConverter.RegisterDocument(requestDocument, new AttributeNamesContainer(attributeNames, typeof(TAttributesObject))); - return new AttributesRegistrationScope(_jsonApiJsonConverter, requestDocument); + return new RequestDocumentRegistrationScope(_jsonApiJsonConverter, requestDocument); } private static Expression RemoveConvert(Expression expression) @@ -67,38 +69,38 @@ private static Expression RemoveConvert(Expression expression) private sealed class JsonApiJsonConverter : JsonConverter { - private readonly Dictionary _alwaysIncludedAttributesPerRequestDocumentInstance = new(); - private readonly Dictionary> _requestDocumentInstancesPerRequestDocumentType = new(); - private bool _isSerializing; + private readonly Dictionary _alwaysIncludedAttributesByRequestDocument = new(); + private readonly Dictionary> _requestDocumentsByType = new(); + private SerializationScope? _serializationScope; public override bool CanRead => false; - public void RegisterRequestDocument(object requestDocument, AttributeNamesContainer attributes) + public void RegisterDocument(object requestDocument, AttributeNamesContainer alwaysIncludedAttributes) { - _alwaysIncludedAttributesPerRequestDocumentInstance[requestDocument] = attributes; + _alwaysIncludedAttributesByRequestDocument[requestDocument] = alwaysIncludedAttributes; 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 (_alwaysIncludedAttributesByRequestDocument.ContainsKey(requestDocument)) { - _alwaysIncludedAttributesPerRequestDocumentInstance.Remove(requestDocument); + _alwaysIncludedAttributesByRequestDocument.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,52 +109,176 @@ 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 + { + AttributeNamesContainer? 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 (_alwaysIncludedAttributesByRequestDocument.TryGetValue(value, out AttributeNamesContainer? attributesObjectInfo)) + { + _serializationScope.AttributesObjectInScope = attributesObjectInfo; + } + + try + { + serializer.Serialize(writer, value); + } + finally + { + _serializationScope = null; + } + } + + private static void AssertObjectMatchesSerializationScope([SysNotNull] AttributeNamesContainer? attributesObjectInfo, object value) + { + Type objectType = value.GetType(); + + if (attributesObjectInfo == null || !attributesObjectInfo.MatchesAttributesObjectType(objectType)) + { + throw new UnreachableCodeException(); + } + } + + private static void SerializeAttributesObject(AttributeNamesContainer 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, AttributeNamesContainer alwaysIncludedAttributes, JsonWriter jsonWriter) + { + PropertyInfo[] propertyInfos = value.GetType().GetProperties(); + + foreach (PropertyInfo attributesPropertyInfo in propertyInfos) + { + bool isExplicitlyIncluded = alwaysIncludedAttributes.ContainsAttribute(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 is not (Required.Always or Required.AllowNull)) + { + return; + } + + bool isPropertyIgnored = DefaultValueEqualsCurrentValue(attribute, value); + + if (isPropertyIgnored) + { + throw new InvalidOperationException($"The following property should not be omitted: {path}.{jsonPropertyForAttribute.PropertyName}."); + } + } + + 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 AttributeNamesContainer? AttributesObjectInScope { get; set; } + + public bool ShouldConvertAsAttributesObject(Type type) + { + if (!_isFirstAttemptToConvertAttributes || AttributesObjectInScope == null) + { + return false; } + + if (!AttributesObjectInScope.MatchesAttributesObjectType(type)) + { + return false; + } + + _isFirstAttemptToConvertAttributes = false; + return true; } } private sealed class AttributeNamesContainer { private readonly ISet _attributeNames; - private readonly Type _containerType; + private readonly Type _attributesObjectType; - public AttributeNamesContainer(ISet attributeNames, Type containerType) + public AttributeNamesContainer(ISet attributeNames, Type attributesObjectType) { ArgumentGuard.NotNull(attributeNames); - ArgumentGuard.NotNull(containerType); + ArgumentGuard.NotNull(attributesObjectType); _attributeNames = attributeNames; - _containerType = containerType; + _attributesObjectType = attributesObjectType; } public bool ContainsAttribute(string name) @@ -160,18 +286,18 @@ public bool ContainsAttribute(string name) return _attributeNames.Contains(name); } - public bool ContainerMatchesType(Type type) + public bool MatchesAttributesObjectType(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,30 @@ 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 AttributeNamesContainer _alwaysIncludedAttributes; - public JsonApiDocumentContractResolver(AttributeNamesContainer attributeNamesContainer) + public JsonApiAttributeContractResolver(AttributeNamesContainer alwaysIncludedAttributes) { - ArgumentGuard.NotNull(attributeNamesContainer); + ArgumentGuard.NotNull(alwaysIncludedAttributes); - _attributeNamesContainer = attributeNamesContainer; + _alwaysIncludedAttributes = alwaysIncludedAttributes; } protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) { JsonProperty property = base.CreateProperty(member, memberSerialization); - if (_attributeNamesContainer.ContainerMatchesType(property.DeclaringType!)) + bool canOmitAttribute = property.Required != Required.Always; + + if (canOmitAttribute && _alwaysIncludedAttributes.MatchesAttributesObjectType(property.DeclaringType!)) { - if (_attributeNamesContainer.ContainsAttribute(property.UnderlyingName!)) + if (_alwaysIncludedAttributes.ContainsAttribute(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/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs index 98ae9938b4..16b173dd17 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs @@ -22,13 +22,14 @@ internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescrip public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors(); - public JsonApiActionDescriptorCollectionProvider(IControllerResourceMapping controllerResourceMapping, IActionDescriptorCollectionProvider defaultProvider) + public JsonApiActionDescriptorCollectionProvider(IControllerResourceMapping controllerResourceMapping, IActionDescriptorCollectionProvider defaultProvider, + ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(controllerResourceMapping); ArgumentGuard.NotNull(defaultProvider); _defaultProvider = defaultProvider; - _jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(controllerResourceMapping); + _jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(controllerResourceMapping, resourceFieldValidationMetadataProvider); } private ActionDescriptorCollection GetActionDescriptors() diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs index d0fe2f1234..0dcb87cabb 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs @@ -15,11 +15,13 @@ internal sealed class JsonApiEndpointMetadataProvider { private readonly IControllerResourceMapping _controllerResourceMapping; private readonly EndpointResolver _endpointResolver = new(); + private readonly NonPrimaryDocumentTypeFactory _nonPrimaryDocumentTypeFactory; - public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping) + public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping, + ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(controllerResourceMapping); - + _nonPrimaryDocumentTypeFactory = new NonPrimaryDocumentTypeFactory(resourceFieldValidationMetadataProvider); _controllerResourceMapping = controllerResourceMapping; } @@ -85,12 +87,12 @@ private static PrimaryRequestMetadata GetPatchRequestMetadata(Type resourceClrTy return new PrimaryRequestMetadata(documentType); } - private static RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable relationships, bool ignoreHasOneRelationships) + private RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable relationships, bool ignoreHasOneRelationships) { IEnumerable relationshipsOfEndpoint = ignoreHasOneRelationships ? relationships.OfType() : relationships; IDictionary requestDocumentTypesByRelationshipName = relationshipsOfEndpoint.ToDictionary(relationship => relationship.PublicName, - NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipRequest); + _nonPrimaryDocumentTypeFactory.GetForRelationshipRequest); return new RelationshipRequestMetadata(requestDocumentTypesByRelationshipName); } @@ -129,18 +131,18 @@ private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type resourceC return new PrimaryResponseMetadata(documentType); } - private static SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable relationships) + private SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable relationships) { IDictionary responseDocumentTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, - NonPrimaryDocumentTypeFactory.Instance.GetForSecondaryResponse); + _nonPrimaryDocumentTypeFactory.GetForSecondaryResponse); return new SecondaryResponseMetadata(responseDocumentTypesByRelationshipName); } - private static RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable relationships) + private RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable relationships) { IDictionary responseDocumentTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, - NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipResponse); + _nonPrimaryDocumentTypeFactory.GetForRelationshipResponse); return new RelationshipResponseMetadata(responseDocumentTypesByRelationshipName); } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NonPrimaryDocumentTypeFactory.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NonPrimaryDocumentTypeFactory.cs index 95d7e2f6e5..387c181f8b 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NonPrimaryDocumentTypeFactory.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NonPrimaryDocumentTypeFactory.cs @@ -15,10 +15,13 @@ internal sealed class NonPrimaryDocumentTypeFactory private static readonly DocumentOpenTypes RelationshipResponseDocumentOpenTypes = new(typeof(ResourceIdentifierCollectionResponseDocument<>), typeof(NullableResourceIdentifierResponseDocument<>), typeof(ResourceIdentifierResponseDocument<>)); - public static NonPrimaryDocumentTypeFactory Instance { get; } = new(); + private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider; - private NonPrimaryDocumentTypeFactory() + public NonPrimaryDocumentTypeFactory(ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { + ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider); + + _resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider; } public Type GetForSecondaryResponse(RelationshipAttribute relationship) @@ -42,13 +45,13 @@ public Type GetForRelationshipResponse(RelationshipAttribute relationship) return Get(relationship, RelationshipResponseDocumentOpenTypes); } - private static Type Get(RelationshipAttribute relationship, DocumentOpenTypes types) + private Type Get(RelationshipAttribute relationship, DocumentOpenTypes types) { // @formatter:nested_ternary_style expanded Type documentOpenType = relationship is HasManyAttribute ? types.ManyDataOpenType - : relationship.IsNullable() + : _resourceFieldValidationMetadataProvider.IsNullable(relationship) ? types.NullableSingleDataOpenType : types.SingleDataOpenType; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipTypeFactory.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipTypeFactory.cs index 7593c9f9f5..06efd7622f 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipTypeFactory.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipTypeFactory.cs @@ -5,17 +5,20 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects; internal sealed class RelationshipTypeFactory { - public static RelationshipTypeFactory Instance { get; } = new(); + private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider; + private readonly NonPrimaryDocumentTypeFactory _nonPrimaryDocumentTypeFactory; - private RelationshipTypeFactory() + public RelationshipTypeFactory(ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { + _nonPrimaryDocumentTypeFactory = new NonPrimaryDocumentTypeFactory(resourceFieldValidationMetadataProvider); + _resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider; } public Type GetForRequest(RelationshipAttribute relationship) { ArgumentGuard.NotNull(relationship); - return NonPrimaryDocumentTypeFactory.Instance.GetForRelationshipRequest(relationship); + return _nonPrimaryDocumentTypeFactory.GetForRelationshipRequest(relationship); } public Type GetForResponse(RelationshipAttribute relationship) @@ -26,7 +29,7 @@ public Type GetForResponse(RelationshipAttribute relationship) Type relationshipDataOpenType = relationship is HasManyAttribute ? typeof(ToManyRelationshipInResponse<>) - : relationship.IsNullable() + : _resourceFieldValidationMetadataProvider.IsNullable(relationship) ? typeof(NullableToOneRelationshipInResponse<>) : typeof(ToOneRelationshipInResponse<>); diff --git a/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs b/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs deleted file mode 100644 index f13c91e837..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace JsonApiDotNetCore.OpenApi; - -internal static class MemberInfoExtensions -{ - public static TypeCategory GetTypeCategory(this MemberInfo source) - { - ArgumentGuard.NotNull(source); - - Type memberType; - - if (source.MemberType.HasFlag(MemberTypes.Field)) - { - memberType = ((FieldInfo)source).FieldType; - } - else if (source.MemberType.HasFlag(MemberTypes.Property)) - { - memberType = ((PropertyInfo)source).PropertyType; - } - else - { - throw new NotSupportedException($"Member type '{source.MemberType}' must be a property or field."); - } - - if (memberType.IsValueType) - { - return Nullable.GetUnderlyingType(memberType) != null ? TypeCategory.NullableValueType : TypeCategory.ValueType; - } - - // Once we switch to .NET 6, we should rely instead on the built-in reflection APIs for nullability information. - // See https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-7/#libraries-reflection-apis-for-nullability-information. - return source.IsNonNullableReferenceType() ? TypeCategory.NonNullableReferenceType : TypeCategory.NullableReferenceType; - } -} diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiSchemaExtensions.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiSchemaExtensions.cs new file mode 100644 index 0000000000..e86c2c7423 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiSchemaExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.OpenApi.Models; + +namespace JsonApiDotNetCore.OpenApi; + +internal static class OpenApiSchemaExtensions +{ + public static OpenApiSchema UnwrapExtendedReferenceSchema(this OpenApiSchema source) + { + ArgumentGuard.NotNull(source); + + if (source.AllOf.Count != 1) + { + throw new InvalidOperationException($"Schema '{nameof(source)}' should not contain multiple entries in '{nameof(source.AllOf)}' "); + } + + return source.AllOf.Single(); + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs deleted file mode 100644 index 32bf9f5187..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using JsonApiDotNetCore.Resources.Annotations; -using Swashbuckle.AspNetCore.SwaggerGen; - -namespace JsonApiDotNetCore.OpenApi; - -internal static class ResourceFieldAttributeExtensions -{ - public static bool IsNullable(this ResourceFieldAttribute source) - { - TypeCategory fieldTypeCategory = source.Property.GetTypeCategory(); - bool hasRequiredAttribute = source.Property.HasAttribute(); - - return fieldTypeCategory switch - { - TypeCategory.NonNullableReferenceType or TypeCategory.ValueType => false, - TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => !hasRequiredAttribute, - _ => throw new UnreachableCodeException() - }; - } -} diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs new file mode 100644 index 0000000000..bfbe32bb31 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs @@ -0,0 +1,92 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi; + +internal sealed class ResourceFieldValidationMetadataProvider +{ + private readonly bool _validateModelState; + private readonly NullabilityInfoContext _nullabilityContext; + private readonly IModelMetadataProvider _modelMetadataProvider; + + public ResourceFieldValidationMetadataProvider(IJsonApiOptions options, IModelMetadataProvider modelMetadataProvider) + { + ArgumentGuard.NotNull(options); + ArgumentGuard.NotNull(modelMetadataProvider); + + _validateModelState = options.ValidateModelState; + _modelMetadataProvider = modelMetadataProvider; + _nullabilityContext = new NullabilityInfoContext(); + } + + public bool IsNullable(ResourceFieldAttribute field) + { + ArgumentGuard.NotNull(field); + + if (field is HasManyAttribute) + { + return false; + } + + bool hasRequiredAttribute = field.Property.HasAttribute(); + NullabilityInfo nullabilityInfo = _nullabilityContext.Create(field.Property); + + if (field is HasManyAttribute) + { + return false; + } + + if (hasRequiredAttribute && _validateModelState && nullabilityInfo.ReadState != NullabilityState.NotNull) + { + return false; + } + + return nullabilityInfo.ReadState != NullabilityState.NotNull; + } + + public bool IsRequired(ResourceFieldAttribute field) + { + ArgumentGuard.NotNull(field); + + bool hasRequiredAttribute = field.Property.HasAttribute(); + + if (!_validateModelState) + { + return hasRequiredAttribute; + } + + if (field is HasManyAttribute) + { + return false; + } + + bool isNotNull = HasNullabilityStateNotNull(field); + bool isRequiredValueType = field.Property.PropertyType.IsValueType && hasRequiredAttribute && isNotNull; + + if (isRequiredValueType) + { + return false; + } + + return IsModelStateValidationRequired(field); + } + + private bool IsModelStateValidationRequired(ResourceFieldAttribute field) + { + ModelMetadata resourceFieldModelMetadata = _modelMetadataProvider.GetMetadataForProperties(field.Type.ClrType) + .Single(modelMetadata => modelMetadata.PropertyName! == field.Property.Name); + + return resourceFieldModelMetadata.ValidatorMetadata.Any(validatorMetadata => validatorMetadata is RequiredAttribute); + } + + private bool HasNullabilityStateNotNull(ResourceFieldAttribute field) + { + NullabilityInfo resourceFieldNullabilityInfo = _nullabilityContext.Create(field.Property); + bool hasNullabilityStateNotNull = resourceFieldNullabilityInfo is { ReadState: NullabilityState.NotNull, WriteState: NullabilityState.NotNull }; + return hasNullabilityStateNotNull; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index f223c7d994..9a76adb7e5 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -34,13 +34,18 @@ public static void AddOpenApi(this IServiceCollection services, IMvcCoreBuilder private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBuilder mvcBuilder) { + services.AddSingleton(); + services.AddSingleton(provider => { var controllerResourceMapping = provider.GetRequiredService(); var actionDescriptorCollectionProvider = provider.GetRequiredService(); var apiDescriptionProviders = provider.GetRequiredService>(); + var resourceFieldValidationMetadataProvider = provider.GetRequiredService(); + + JsonApiActionDescriptorCollectionProvider descriptorCollectionProviderWrapper = + new(controllerResourceMapping, actionDescriptorCollectionProvider, resourceFieldValidationMetadataProvider); - JsonApiActionDescriptorCollectionProvider descriptorCollectionProviderWrapper = new(controllerResourceMapping, actionDescriptorCollectionProvider); return new ApiDescriptionGroupCollectionProvider(descriptorCollectionProviderWrapper, apiDescriptionProviders); }); @@ -64,6 +69,7 @@ private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection SetOperationInfo(swaggerGenOptions, controllerResourceMapping, namingPolicy); SetSchemaIdSelector(swaggerGenOptions, resourceGraph, namingPolicy); swaggerGenOptions.DocumentFilter(); + swaggerGenOptions.UseAllOfToExtendReferenceSchemas(); setupSwaggerGenAction?.Invoke(swaggerGenOptions); }); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index b4abe196ca..b2bf21db1b 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -27,20 +27,28 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator typeof(NullableToOneRelationshipInRequest<>) }; + private static readonly Type[] JsonApiDocumentWithNullableDataOpenTypes = + { + typeof(NullableSecondaryResourceResponseDocument<>), + typeof(NullableResourceIdentifierResponseDocument<>), + typeof(NullableToOneRelationshipInRequest<>) + }; + private readonly ISchemaGenerator _defaultSchemaGenerator; private readonly ResourceObjectSchemaGenerator _resourceObjectSchemaGenerator; - private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator; private readonly SchemaRepositoryAccessor _schemaRepositoryAccessor = new(); - public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options) + public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options, + ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(defaultSchemaGenerator); ArgumentGuard.NotNull(resourceGraph); ArgumentGuard.NotNull(options); _defaultSchemaGenerator = defaultSchemaGenerator; - _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor, options.SerializerOptions.PropertyNamingPolicy); - _resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor); + + _resourceObjectSchemaGenerator = new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceGraph, options, _schemaRepositoryAccessor, + resourceFieldValidationMetadataProvider); } public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepository, MemberInfo? memberInfo = null, ParameterInfo? parameterInfo = null, @@ -53,17 +61,23 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos if (schemaRepository.TryLookupByType(modelType, out OpenApiSchema jsonApiDocumentSchema)) { - return jsonApiDocumentSchema; + // For unknown reasons, Swashbuckle chooses to wrap root request bodies, but not response bodies. See + // https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/861#issuecomment-1373631712 + return memberInfo != null || parameterInfo != null + ? _defaultSchemaGenerator.GenerateSchema(modelType, schemaRepository, memberInfo, parameterInfo) + : jsonApiDocumentSchema; } if (IsJsonApiDocument(modelType)) { OpenApiSchema schema = GenerateJsonApiDocumentSchema(modelType); - if (IsDataPropertyNullable(modelType)) + if (IsDataPropertyNullableInDocument(modelType)) { SetDataObjectSchemaToNullable(schema); } + + // Schema might depend on other schemas not handled by us, so should not return here. } return _defaultSchemaGenerator.GenerateSchema(modelType, schemaRepository, memberInfo, parameterInfo); @@ -88,7 +102,7 @@ private OpenApiSchema GenerateJsonApiDocumentSchema(Type documentType) OpenApiSchema referenceSchemaForDataObject = IsManyDataDocument(documentType) ? CreateArrayTypeDataSchema(referenceSchemaForResourceObject) - : referenceSchemaForResourceObject; + : CreateExtendedReferenceSchema(referenceSchemaForResourceObject); fullSchemaForDocument.Properties[JsonApiObjectPropertyName.Data] = referenceSchemaForDataObject; @@ -100,25 +114,19 @@ private static bool IsManyDataDocument(Type documentType) return documentType.BaseType!.GetGenericTypeDefinition() == typeof(ManyData<>); } - private static bool IsDataPropertyNullable(Type type) + private static bool IsDataPropertyNullableInDocument(Type documentType) { - PropertyInfo? dataProperty = type.GetProperty(nameof(JsonApiObjectPropertyName.Data)); - - if (dataProperty == null) - { - throw new UnreachableCodeException(); - } - - TypeCategory typeCategory = dataProperty.GetTypeCategory(); + Type documentOpenType = documentType.GetGenericTypeDefinition(); - return typeCategory == TypeCategory.NullableReferenceType; + return JsonApiDocumentWithNullableDataOpenTypes.Contains(documentOpenType); } private void SetDataObjectSchemaToNullable(OpenApiSchema referenceSchemaForDocument) { OpenApiSchema fullSchemaForDocument = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForDocument.Reference.Id]; OpenApiSchema referenceSchemaForData = fullSchemaForDocument.Properties[JsonApiObjectPropertyName.Data]; - fullSchemaForDocument.Properties[JsonApiObjectPropertyName.Data] = _nullableReferenceSchemaGenerator.GenerateSchema(referenceSchemaForData); + referenceSchemaForData.Nullable = true; + fullSchemaForDocument.Properties[JsonApiObjectPropertyName.Data] = referenceSchemaForData; } private static OpenApiSchema CreateArrayTypeDataSchema(OpenApiSchema referenceSchemaForResourceObject) @@ -129,4 +137,15 @@ private static OpenApiSchema CreateArrayTypeDataSchema(OpenApiSchema referenceSc Type = "array" }; } + + private static OpenApiSchema CreateExtendedReferenceSchema(OpenApiSchema referenceSchemaForResourceObject) + { + return new OpenApiSchema + { + AllOf = new List + { + referenceSchemaForResourceObject + } + }; + } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs deleted file mode 100644 index 4e09769e87..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System.Text.Json; -using Microsoft.OpenApi.Models; - -namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; - -internal sealed class NullableReferenceSchemaGenerator -{ - private const string PascalCaseNullableSchemaReferenceId = "NullValue"; - - private readonly NullableReferenceSchemaStrategy _nullableReferenceStrategy = - Enum.Parse(NullableReferenceSchemaStrategy.Implicit.ToString()); - - private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; - private readonly string _nullableSchemaReferenceId; - - private OpenApiSchema? _referenceSchemaForExplicitNullValue; - - public NullableReferenceSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor, JsonNamingPolicy? namingPolicy) - { - ArgumentGuard.NotNull(schemaRepositoryAccessor); - - _schemaRepositoryAccessor = schemaRepositoryAccessor; - - _nullableSchemaReferenceId = namingPolicy != null ? namingPolicy.ConvertName(PascalCaseNullableSchemaReferenceId) : PascalCaseNullableSchemaReferenceId; - } - - public OpenApiSchema GenerateSchema(OpenApiSchema referenceSchema) - { - ArgumentGuard.NotNull(referenceSchema); - - return new OpenApiSchema - { - OneOf = new List - { - referenceSchema, - _nullableReferenceStrategy == NullableReferenceSchemaStrategy.Explicit ? GetExplicitNullSchema() : GetImplicitNullSchema() - } - }; - } - - // This approach is supported in OAS starting from v3.1. See https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-580103688 - private static OpenApiSchema GetExplicitNullSchema() - { - return new OpenApiSchema - { - Type = "null" - }; - } - - // This approach is supported starting from OAS v3.0. See https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-487314681 - private OpenApiSchema GetImplicitNullSchema() - { - EnsureFullSchemaForNullValueExists(); - - return _referenceSchemaForExplicitNullValue ??= new OpenApiSchema - { - Reference = new OpenApiReference - { - Id = _nullableSchemaReferenceId, - Type = ReferenceType.Schema - } - }; - } - - private void EnsureFullSchemaForNullValueExists() - { - if (!_schemaRepositoryAccessor.Current.Schemas.ContainsKey(_nullableSchemaReferenceId)) - { - var fullSchemaForNullValue = new OpenApiSchema - { - Nullable = true, - Not = new OpenApiSchema - { - AnyOf = new List - { - new() - { - Type = "string" - }, - new() - { - Type = "number" - }, - new() - { - Type = "boolean" - }, - new() - { - Type = "object" - }, - new() - { - Type = "array" - } - }, - Items = new OpenApiSchema() - } - }; - - _schemaRepositoryAccessor.Current.AddDefinition(_nullableSchemaReferenceId, fullSchemaForNullValue); - } - } -} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaStrategy.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaStrategy.cs deleted file mode 100644 index 282b4a282b..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaStrategy.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; - -internal enum NullableReferenceSchemaStrategy -{ - Implicit, - Explicit -} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs index 48e65d721c..6959e17284 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs @@ -1,6 +1,4 @@ -using System.ComponentModel.DataAnnotations; using System.Reflection; -using System.Text.Json; using JsonApiDotNetCore.OpenApi.JsonApiObjects; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; @@ -12,35 +10,46 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; internal sealed class ResourceFieldObjectSchemaBuilder { - private static readonly Type[] RelationshipInResponseOpenTypes = + private static readonly Type[] RelationshipSchemaInResponseOpenTypes = { typeof(ToOneRelationshipInResponse<>), typeof(ToManyRelationshipInResponse<>), typeof(NullableToOneRelationshipInResponse<>) }; + private static readonly Type[] NullableRelationshipSchemaOpenTypes = + { + typeof(NullableToOneRelationshipInRequest<>), + typeof(NullableToOneRelationshipInResponse<>) + }; + private readonly ResourceTypeInfo _resourceTypeInfo; private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; private readonly SchemaGenerator _defaultSchemaGenerator; private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator; private readonly SchemaRepository _resourceSchemaRepository = new(); - private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator; private readonly IDictionary _schemasForResourceFields; + private readonly ResourceFieldValidationMetadataProvider _resourceFieldValidationMetadataProvider; + private readonly RelationshipTypeFactory _relationshipTypeFactory; + private readonly NullabilityInfoContext _nullabilityInfoContext = new(); public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISchemaRepositoryAccessor schemaRepositoryAccessor, - SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, JsonNamingPolicy? namingPolicy) + SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, + ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(resourceTypeInfo); ArgumentGuard.NotNull(schemaRepositoryAccessor); ArgumentGuard.NotNull(defaultSchemaGenerator); ArgumentGuard.NotNull(resourceTypeSchemaGenerator); + ArgumentGuard.NotNull(resourceFieldValidationMetadataProvider); _resourceTypeInfo = resourceTypeInfo; _schemaRepositoryAccessor = schemaRepositoryAccessor; _defaultSchemaGenerator = defaultSchemaGenerator; _resourceTypeSchemaGenerator = resourceTypeSchemaGenerator; + _resourceFieldValidationMetadataProvider = resourceFieldValidationMetadataProvider; - _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, namingPolicy); + _relationshipTypeFactory = new RelationshipTypeFactory(resourceFieldValidationMetadataProvider); _schemasForResourceFields = GetFieldSchemas(); } @@ -65,9 +74,17 @@ public void SetMembersOfAttributesObject(OpenApiSchema fullSchemaForAttributesOb if (matchingAttribute != null && matchingAttribute.Capabilities.HasFlag(requiredCapability)) { - AddAttributeSchemaToResourceObject(matchingAttribute, fullSchemaForAttributesObject, resourceFieldSchema); + bool isPrimitiveOpenApiType = resourceFieldSchema.AllOf.IsNullOrEmpty(); + + // Types like enum and and complex attributes are not primitive and handles as reference schemas. + if (!isPrimitiveOpenApiType) + { + EnsureAttributeSchemaIsExposed(resourceFieldSchema, matchingAttribute); + } - resourceFieldSchema.Nullable = matchingAttribute.IsNullable(); + fullSchemaForAttributesObject.Properties[matchingAttribute.PublicName] = resourceFieldSchema; + + resourceFieldSchema.Nullable = _resourceFieldValidationMetadataProvider.IsNullable(matchingAttribute); if (IsFieldRequired(matchingAttribute)) { @@ -84,40 +101,40 @@ private static AttrCapabilities GetRequiredCapabilityForAttributes(Type resource resourceObjectOpenType == typeof(ResourceObjectInPatchRequest<>) ? AttrCapabilities.AllowChange : throw new UnreachableCodeException(); } - private void AddAttributeSchemaToResourceObject(AttrAttribute attribute, OpenApiSchema attributesObjectSchema, OpenApiSchema resourceAttributeSchema) + private void EnsureAttributeSchemaIsExposed(OpenApiSchema attributeReferenceSchema, AttrAttribute attribute) { - if (resourceAttributeSchema.Reference != null && !_schemaRepositoryAccessor.Current.TryLookupByType(attribute.Property.PropertyType, out _)) + Type nonNullableTypeInPropertyType = GetRepresentedTypeForAttributeSchema(attribute); + + if (_schemaRepositoryAccessor.Current.TryLookupByType(nonNullableTypeInPropertyType, out _)) { - ExposeSchema(resourceAttributeSchema.Reference, attribute.Property.PropertyType); + return; } - attributesObjectSchema.Properties.Add(attribute.PublicName, resourceAttributeSchema); + string schemaId = attributeReferenceSchema.UnwrapExtendedReferenceSchema().Reference.Id; + + OpenApiSchema fullSchema = _resourceSchemaRepository.Schemas[schemaId]; + _schemaRepositoryAccessor.Current.AddDefinition(schemaId, fullSchema); + _schemaRepositoryAccessor.Current.RegisterType(nonNullableTypeInPropertyType, schemaId); } - private void ExposeSchema(OpenApiReference openApiReference, Type typeRepresentedBySchema) + private Type GetRepresentedTypeForAttributeSchema(AttrAttribute attribute) { - OpenApiSchema fullSchema = _resourceSchemaRepository.Schemas[openApiReference.Id]; - _schemaRepositoryAccessor.Current.AddDefinition(openApiReference.Id, fullSchema); - _schemaRepositoryAccessor.Current.RegisterType(typeRepresentedBySchema, openApiReference.Id); + NullabilityInfo attributeNullabilityInfo = _nullabilityInfoContext.Create(attribute.Property); + + bool isNullable = attributeNullabilityInfo is { ReadState: NullabilityState.Nullable, WriteState: NullabilityState.Nullable }; + + Type nonNullableTypeInPropertyType = isNullable + ? Nullable.GetUnderlyingType(attribute.Property.PropertyType) ?? attribute.Property.PropertyType + : attribute.Property.PropertyType; + + return nonNullableTypeInPropertyType; } private bool IsFieldRequired(ResourceFieldAttribute field) { - if (field is HasManyAttribute || _resourceTypeInfo.ResourceObjectOpenType != typeof(ResourceObjectInPostRequest<>)) - { - return false; - } - - TypeCategory fieldTypeCategory = field.Property.GetTypeCategory(); - bool hasRequiredAttribute = field.Property.HasAttribute(); + bool isSchemaForUpdateResourceEndpoint = _resourceTypeInfo.ResourceObjectOpenType == typeof(ResourceObjectInPatchRequest<>); - return fieldTypeCategory switch - { - TypeCategory.NonNullableReferenceType => true, - TypeCategory.ValueType => hasRequiredAttribute, - TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => hasRequiredAttribute, - _ => throw new UnreachableCodeException() - }; + return !isSchemaForUpdateResourceEndpoint && _resourceFieldValidationMetadataProvider.IsRequired(field); } public void SetMembersOfRelationshipsObject(OpenApiSchema fullSchemaForRelationshipsObject) @@ -165,9 +182,18 @@ private void AddRelationshipSchemaToResourceObject(RelationshipAttribute relatio { Type relationshipSchemaType = GetRelationshipSchemaType(relationship, _resourceTypeInfo.ResourceObjectOpenType); - OpenApiSchema relationshipSchema = GetReferenceSchemaForRelationship(relationshipSchemaType) ?? CreateRelationshipSchema(relationshipSchemaType); + OpenApiSchema referenceSchemaForRelationship = + GetReferenceSchemaForRelationship(relationshipSchemaType) ?? CreateRelationshipReferenceSchema(relationshipSchemaType); + + var extendedReferenceSchemaForRelationship = new OpenApiSchema + { + AllOf = new List + { + referenceSchemaForRelationship + } + }; - fullSchemaForRelationshipsObject.Properties.Add(relationship.PublicName, relationshipSchema); + fullSchemaForRelationshipsObject.Properties.Add(relationship.PublicName, extendedReferenceSchemaForRelationship); if (IsFieldRequired(relationship)) { @@ -175,11 +201,11 @@ private void AddRelationshipSchemaToResourceObject(RelationshipAttribute relatio } } - private static Type GetRelationshipSchemaType(RelationshipAttribute relationship, Type resourceObjectType) + private Type GetRelationshipSchemaType(RelationshipAttribute relationship, Type resourceObjectType) { return resourceObjectType.GetGenericTypeDefinition().IsAssignableTo(typeof(ResourceObjectInResponse<>)) - ? RelationshipTypeFactory.Instance.GetForResponse(relationship) - : RelationshipTypeFactory.Instance.GetForRequest(relationship); + ? _relationshipTypeFactory.GetForResponse(relationship) + : _relationshipTypeFactory.GetForRequest(relationship); } private OpenApiSchema? GetReferenceSchemaForRelationship(Type relationshipSchemaType) @@ -188,16 +214,15 @@ private static Type GetRelationshipSchemaType(RelationshipAttribute relationship return referenceSchema; } - private OpenApiSchema CreateRelationshipSchema(Type relationshipSchemaType) + private OpenApiSchema CreateRelationshipReferenceSchema(Type relationshipSchemaType) { OpenApiSchema referenceSchema = _defaultSchemaGenerator.GenerateSchema(relationshipSchemaType, _schemaRepositoryAccessor.Current); OpenApiSchema fullSchema = _schemaRepositoryAccessor.Current.Schemas[referenceSchema.Reference.Id]; - if (IsDataPropertyNullable(relationshipSchemaType)) + if (IsDataPropertyNullableInRelationshipSchemaType(relationshipSchemaType)) { - fullSchema.Properties[JsonApiObjectPropertyName.Data] = - _nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiObjectPropertyName.Data]); + fullSchema.Properties[JsonApiObjectPropertyName.Data].Nullable = true; } if (IsRelationshipInResponseType(relationshipSchemaType)) @@ -212,18 +237,12 @@ private static bool IsRelationshipInResponseType(Type relationshipSchemaType) { Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition(); - return RelationshipInResponseOpenTypes.Contains(relationshipSchemaOpenType); + return RelationshipSchemaInResponseOpenTypes.Contains(relationshipSchemaOpenType); } - private static bool IsDataPropertyNullable(Type type) + private static bool IsDataPropertyNullableInRelationshipSchemaType(Type relationshipSchemaType) { - PropertyInfo? dataProperty = type.GetProperty(nameof(JsonApiObjectPropertyName.Data)); - - if (dataProperty == null) - { - throw new UnreachableCodeException(); - } - - return dataProperty.GetTypeCategory() == TypeCategory.NullableReferenceType; + Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition(); + return NullableRelationshipSchemaOpenTypes.Contains(relationshipSchemaOpenType); } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index 090fcea2dd..ca6cfafdab 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -15,7 +15,7 @@ internal sealed class ResourceObjectSchemaGenerator private readonly Func _resourceFieldObjectSchemaBuilderFactory; public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options, - ISchemaRepositoryAccessor schemaRepositoryAccessor) + ISchemaRepositoryAccessor schemaRepositoryAccessor, ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider) { ArgumentGuard.NotNull(defaultSchemaGenerator); ArgumentGuard.NotNull(resourceGraph); @@ -31,7 +31,7 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe _allowClientGeneratedIds = options.AllowClientGeneratedIds; _resourceFieldObjectSchemaBuilderFactory = resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor, - defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy); + defaultSchemaGenerator, _resourceTypeSchemaGenerator, resourceFieldValidationMetadataProvider); } public OpenApiSchema GenerateSchema(Type resourceObjectType) @@ -92,7 +92,9 @@ private void SetResourceType(OpenApiSchema fullSchemaForResourceObject, Type res private void SetResourceAttributes(OpenApiSchema fullSchemaForResourceObject, ResourceFieldObjectSchemaBuilder builder) { - OpenApiSchema referenceSchemaForAttributesObject = fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.AttributesObject]; + OpenApiSchema referenceSchemaForAttributesObject = + fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.AttributesObject].UnwrapExtendedReferenceSchema(); + OpenApiSchema fullSchemaForAttributesObject = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForAttributesObject.Reference.Id]; builder.SetMembersOfAttributesObject(fullSchemaForAttributesObject); @@ -110,7 +112,9 @@ private void SetResourceAttributes(OpenApiSchema fullSchemaForResourceObject, Re private void SetResourceRelationships(OpenApiSchema fullSchemaForResourceObject, ResourceFieldObjectSchemaBuilder builder) { - OpenApiSchema referenceSchemaForRelationshipsObject = fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.RelationshipsObject]; + OpenApiSchema referenceSchemaForRelationshipsObject = + fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.RelationshipsObject].UnwrapExtendedReferenceSchema(); + OpenApiSchema fullSchemaForRelationshipsObject = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForRelationshipsObject.Reference.Id]; builder.SetMembersOfRelationshipsObject(fullSchemaForRelationshipsObject); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs index 592a8ed860..6db24901b2 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs @@ -28,9 +28,9 @@ public OpenApiSchema Get(Type resourceClrType) { ArgumentGuard.NotNull(resourceClrType); - if (_resourceClrTypeSchemaCache.TryGetValue(resourceClrType, out OpenApiSchema? referenceSchema)) + if (_resourceClrTypeSchemaCache.TryGetValue(resourceClrType, out OpenApiSchema? extendedReferenceSchema)) { - return referenceSchema; + return extendedReferenceSchema; } ResourceType resourceType = _resourceGraph.GetResourceType(resourceClrType); @@ -46,7 +46,7 @@ public OpenApiSchema Get(Type resourceClrType) string schemaId = GetSchemaId(resourceType); - referenceSchema = new OpenApiSchema + var referenceSchema = new OpenApiSchema { Reference = new OpenApiReference { @@ -55,10 +55,18 @@ public OpenApiSchema Get(Type resourceClrType) } }; - _schemaRepositoryAccessor.Current.AddDefinition(referenceSchema.Reference.Id, fullSchema); - _resourceClrTypeSchemaCache.Add(resourceType.ClrType, referenceSchema); + extendedReferenceSchema = new OpenApiSchema + { + AllOf = new List + { + referenceSchema + } + }; + + _schemaRepositoryAccessor.Current.AddDefinition(schemaId, fullSchema); + _resourceClrTypeSchemaCache.Add(resourceType.ClrType, extendedReferenceSchema); - return referenceSchema; + return extendedReferenceSchema; } private string GetSchemaId(ResourceType resourceType) diff --git a/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs b/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs deleted file mode 100644 index 1641e31775..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace JsonApiDotNetCore.OpenApi; - -internal enum TypeCategory -{ - NonNullableReferenceType, - NullableReferenceType, - ValueType, - NullableValueType -} diff --git a/test/OpenApiClientTests/LegacyClient/ApiResponse.cs b/test/OpenApiClientTests/ApiResponse.cs similarity index 94% rename from test/OpenApiClientTests/LegacyClient/ApiResponse.cs rename to test/OpenApiClientTests/ApiResponse.cs index e62353d17a..7ea398c73a 100644 --- a/test/OpenApiClientTests/LegacyClient/ApiResponse.cs +++ b/test/OpenApiClientTests/ApiResponse.cs @@ -3,7 +3,7 @@ #pragma warning disable AV1008 // Class should not be static -namespace OpenApiClientTests.LegacyClient; +namespace OpenApiClientTests; internal static class ApiResponse { diff --git a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs b/test/OpenApiClientTests/FakeHttpClientWrapper.cs similarity index 90% rename from test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs rename to test/OpenApiClientTests/FakeHttpClientWrapper.cs index 8a5501354f..8226a4e496 100644 --- a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs +++ b/test/OpenApiClientTests/FakeHttpClientWrapper.cs @@ -1,9 +1,10 @@ using System.Net; using System.Net.Http.Headers; using System.Text; +using System.Text.Json; using JsonApiDotNetCore.OpenApi.Client; -namespace OpenApiClientTests.LegacyClient; +namespace OpenApiClientTests; /// /// Enables to inject an outgoing response body and inspect the incoming request. @@ -22,6 +23,17 @@ private FakeHttpClientWrapper(HttpClient httpClient, FakeHttpMessageHandler hand _handler = handler; } + public JsonElement ParseRequestBody() + { + if (RequestBody == null) + { + throw new InvalidOperationException(); + } + + using JsonDocument jsonDocument = JsonDocument.Parse(RequestBody); + return jsonDocument.RootElement.Clone(); + } + public static FakeHttpClientWrapper Create(HttpStatusCode statusCode, string? responseBody) { HttpResponseMessage response = CreateResponse(statusCode, responseBody); diff --git a/test/OpenApiClientTests/FakerFactory.cs b/test/OpenApiClientTests/FakerFactory.cs new file mode 100644 index 0000000000..c28adbaa60 --- /dev/null +++ b/test/OpenApiClientTests/FakerFactory.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using AutoBogus; + +namespace OpenApiClientTests; + +internal sealed class FakerFactory +{ + public static FakerFactory Instance { get; } = new(); + + private FakerFactory() + { + } + + public AutoFaker Create() + where TTarget : class + { + return new AutoFaker(); + } + + public AutoFaker CreateForObjectWithResourceId() + where TTarget : class + { + return new AutoFaker().Configure(builder => builder.WithOverride(new ResourceStringIdOverride())); + } + + private sealed class ResourceStringIdOverride : AutoGeneratorOverride + { + private readonly IAutoFaker _idFaker = AutoFaker.Create(); + + public override bool CanOverride(AutoGenerateContext context) + { + PropertyInfo? resourceIdPropertyInfo = context.GenerateType.GetProperty("Id"); + return resourceIdPropertyInfo != null && resourceIdPropertyInfo.PropertyType == typeof(string); + } + + public override void Generate(AutoGenerateOverrideContext context) + { + ((dynamic)context.Instance).Id = _idFaker.Generate()!.ToString()!; + } + } +} diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/RequestDocumentRegistrationLifetimeTests.cs similarity index 79% rename from test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs rename to test/OpenApiClientTests/LegacyClient/RequestDocumentRegistrationLifetimeTests.cs index 4582f9578d..b6855c06d2 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.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(requestDocument1, airplane => airplane.AirtimeInHours)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(requestDocument1, airplane => airplane.IsInMaintenance)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(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.WithPartialAttributeSerialization(requestDocument1, airplane => airplane.SerialNumber)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.WithPartialAttributeSerialization(requestDocument2, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index e03e8f1015..ab58b101e6 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.WithPartialAttributeSerialization(requestDocument, airplane => airplane.SerialNumber)) { // Act @@ -203,7 +203,7 @@ public async Task Partial_patching_resource_produces_expected_request() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.WithPartialAttributeSerialization(requestDocument, airplane => airplane.SerialNumber, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 10f86adca5..b112a23d05 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -149,7 +149,9 @@ public async Task Getting_resource_translates_response() // Arrange const string flightId = "ZvuH1"; const string departsAtInZuluTime = "2021-06-08T12:53:30.554Z"; + const string flightDestination = "Amsterdam"; const string arrivesAtWithUtcOffset = "2019-02-20T11:56:33.0721266+01:00"; + const string flightServiceOnBoard = "Movies"; const string responseBody = @"{ ""links"": { @@ -160,7 +162,9 @@ public async Task Getting_resource_translates_response() ""id"": """ + flightId + @""", ""attributes"": { ""departs-at"": """ + departsAtInZuluTime + @""", - ""arrives-at"": """ + arrivesAtWithUtcOffset + @""" + ""arrives-at"": """ + arrivesAtWithUtcOffset + @""", + ""final-destination"": """ + flightDestination + @""", + ""services-on-board"": [""" + flightServiceOnBoard + @"""] }, ""links"": { ""self"": """ + HostPrefix + @"flights/" + flightId + @""" @@ -181,8 +185,8 @@ public async Task Getting_resource_translates_response() document.Data.Relationships.Should().BeNull(); document.Data.Attributes.DepartsAt.Should().Be(DateTimeOffset.Parse(departsAtInZuluTime)); document.Data.Attributes.ArrivesAt.Should().Be(DateTimeOffset.Parse(arrivesAtWithUtcOffset)); - document.Data.Attributes.ServicesOnBoard.Should().BeNull(); - document.Data.Attributes.FinalDestination.Should().BeNull(); + document.Data.Attributes.ServicesOnBoard.Should().Contain(flightServiceOnBoard); + document.Data.Attributes.FinalDestination.Should().Be(flightDestination); document.Data.Attributes.StopOverDestination.Should().BeNull(); document.Data.Attributes.OperatedBy.Should().Be(default); } diff --git a/test/OpenApiClientTests/LegacyClient/swagger.g.json b/test/OpenApiClientTests/LegacyClient/swagger.g.json index f95214e8d4..f2c0237d1d 100644 --- a/test/OpenApiClientTests/LegacyClient/swagger.g.json +++ b/test/OpenApiClientTests/LegacyClient/swagger.g.json @@ -51,7 +51,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-post-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-post-request-document" + } + ] } } } @@ -149,7 +153,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-patch-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-patch-request-document" + } + ] } } } @@ -326,7 +334,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -356,7 +368,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -386,7 +402,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -444,7 +464,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-post-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-post-request-document" + } + ] } } } @@ -542,7 +566,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-patch-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-patch-request-document" + } + ] } } } @@ -719,7 +747,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -749,7 +781,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -779,7 +815,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -925,7 +965,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -955,7 +999,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -985,7 +1033,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -1043,7 +1095,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-post-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/flight-post-request-document" + } + ] } } } @@ -1141,7 +1197,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-patch-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/flight-patch-request-document" + } + ] } } } @@ -1318,7 +1378,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + } + ] } } } @@ -1464,7 +1528,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] } } } @@ -1494,7 +1562,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] } } } @@ -1524,7 +1596,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] } } } @@ -1670,7 +1746,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } } } @@ -1700,7 +1780,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } } } @@ -1730,7 +1814,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } } } @@ -1876,7 +1964,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + } + ] } } } @@ -1970,6 +2062,9 @@ "additionalProperties": false }, "airplane-attributes-in-response": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -2004,7 +2099,11 @@ "nullable": true }, "kind": { - "$ref": "#/components/schemas/aircraft-kind" + "allOf": [ + { + "$ref": "#/components/schemas/aircraft-kind" + } + ] } }, "additionalProperties": false @@ -2024,13 +2123,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -2043,17 +2150,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/airplane-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/airplane-attributes-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-attributes-in-patch-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/airplane-relationships-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-relationships-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2065,13 +2184,25 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/airplane-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-resource-type" + } + ] }, "attributes": { - "$ref": "#/components/schemas/airplane-attributes-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-attributes-in-post-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/airplane-relationships-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-relationships-in-post-request" + } + ] } }, "additionalProperties": false @@ -2085,24 +2216,40 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/airplane-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/airplane-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-attributes-in-response" + } + ] }, "relationships": { - "$ref": "#/components/schemas/airplane-relationships-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-relationships-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -2114,7 +2261,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/airplane-data-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-data-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2126,7 +2277,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/airplane-data-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-data-in-post-request" + } + ] } }, "additionalProperties": false @@ -2139,17 +2294,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/airplane-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-data-in-response" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -2158,7 +2325,11 @@ "type": "object", "properties": { "flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } }, "additionalProperties": false @@ -2167,7 +2338,11 @@ "type": "object", "properties": { "flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } }, "additionalProperties": false @@ -2176,7 +2351,11 @@ "type": "object", "properties": { "flights": { - "$ref": "#/components/schemas/to-many-flight-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-response" + } + ] } }, "additionalProperties": false @@ -2238,6 +2417,10 @@ "additionalProperties": false }, "flight-attendant-attributes-in-response": { + "required": [ + "email-address", + "profile-image-url" + ], "type": "object", "properties": { "email-address": { @@ -2277,13 +2460,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -2296,17 +2487,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-attendant-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/flight-attendant-attributes-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-attributes-in-patch-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-attendant-relationships-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-relationships-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2318,13 +2521,25 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-attendant-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-resource-type" + } + ] }, "attributes": { - "$ref": "#/components/schemas/flight-attendant-attributes-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-attributes-in-post-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-attendant-relationships-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-relationships-in-post-request" + } + ] } }, "additionalProperties": false @@ -2338,24 +2553,40 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-attendant-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/flight-attendant-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-attributes-in-response" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-attendant-relationships-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-relationships-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -2368,7 +2599,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-attendant-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-resource-type" + } + ] }, "id": { "minLength": 1, @@ -2392,13 +2627,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + } + ] } }, "additionalProperties": false @@ -2411,17 +2654,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + } + ] } }, "additionalProperties": false @@ -2433,7 +2688,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2445,7 +2704,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-post-request" + } + ] } }, "additionalProperties": false @@ -2458,17 +2721,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -2477,10 +2752,18 @@ "type": "object", "properties": { "scheduled-for-flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] }, "purser-on-flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } }, "additionalProperties": false @@ -2489,10 +2772,18 @@ "type": "object", "properties": { "scheduled-for-flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] }, "purser-on-flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } }, "additionalProperties": false @@ -2501,10 +2792,18 @@ "type": "object", "properties": { "scheduled-for-flights": { - "$ref": "#/components/schemas/to-many-flight-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-response" + } + ] }, "purser-on-flights": { - "$ref": "#/components/schemas/to-many-flight-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-response" + } + ] } }, "additionalProperties": false @@ -2523,17 +2822,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -2551,12 +2862,20 @@ "nullable": true }, "operated-by": { - "$ref": "#/components/schemas/airline" + "allOf": [ + { + "$ref": "#/components/schemas/airline" + } + ] } }, "additionalProperties": false }, "flight-attributes-in-response": { + "required": [ + "final-destination", + "services-on-board" + ], "type": "object", "properties": { "final-destination": { @@ -2569,7 +2888,11 @@ "nullable": true }, "operated-by": { - "$ref": "#/components/schemas/airline" + "allOf": [ + { + "$ref": "#/components/schemas/airline" + } + ] }, "departs-at": { "type": "string", @@ -2605,13 +2928,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -2624,17 +2955,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/flight-attributes-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attributes-in-patch-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-relationships-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-relationships-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2646,10 +2989,18 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-resource-type" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-relationships-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-relationships-in-post-request" + } + ] } }, "additionalProperties": false @@ -2663,24 +3014,40 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/flight-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attributes-in-response" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-relationships-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-relationships-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -2693,7 +3060,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-resource-type" + } + ] }, "id": { "minLength": 1, @@ -2717,13 +3088,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + } + ] } }, "additionalProperties": false @@ -2735,7 +3114,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-data-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-data-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2747,7 +3130,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-data-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-data-in-post-request" + } + ] } }, "additionalProperties": false @@ -2760,17 +3147,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-data-in-response" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -2779,16 +3178,32 @@ "type": "object", "properties": { "cabin-crew-members": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] }, "purser": { - "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + } + ] }, "backup-purser": { - "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + } + ] }, "passengers": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } }, "additionalProperties": false @@ -2800,34 +3215,69 @@ "type": "object", "properties": { "cabin-crew-members": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] }, "purser": { - "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + } + ] }, "backup-purser": { - "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + } + ] }, "passengers": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } }, "additionalProperties": false }, "flight-relationships-in-response": { + "required": [ + "purser" + ], "type": "object", "properties": { "cabin-crew-members": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-response" + } + ] }, "purser": { - "$ref": "#/components/schemas/to-one-flight-attendant-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-flight-attendant-in-response" + } + ] }, "backup-purser": { - "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-response" + } + ] }, "passengers": { - "$ref": "#/components/schemas/to-many-passenger-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-response" + } + ] } }, "additionalProperties": false @@ -2858,7 +3308,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -2996,29 +3446,6 @@ }, "additionalProperties": false }, - "null-value": { - "not": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "object" - }, - { - "type": "array" - } - ], - "items": { } - }, - "nullable": true - }, "nullable-flight-attendant-identifier-response-document": { "required": [ "data", @@ -3027,24 +3454,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + } + ] } }, "additionalProperties": false @@ -3057,24 +3490,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/flight-attendant-data-in-response" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -3086,14 +3525,12 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true } }, "additionalProperties": false @@ -3105,21 +3542,23 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -3132,7 +3571,11 @@ "nullable": true }, "cabin-area": { - "$ref": "#/components/schemas/cabin-area" + "allOf": [ + { + "$ref": "#/components/schemas/cabin-area" + } + ] } }, "additionalProperties": false @@ -3152,13 +3595,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -3172,21 +3623,33 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/passenger-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/passenger-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/passenger-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/passenger-attributes-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -3199,7 +3662,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/passenger-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/passenger-resource-type" + } + ] }, "id": { "minLength": 1, @@ -3223,13 +3690,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + } + ] } }, "additionalProperties": false @@ -3268,11 +3743,15 @@ } }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -3305,11 +3784,15 @@ } }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -3342,11 +3825,15 @@ } }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -3358,7 +3845,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + ] } }, "additionalProperties": false @@ -3370,18 +3861,26 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false } } } -} \ No newline at end of file +} diff --git a/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json index d2850c032d..b356959a8d 100644 --- a/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/CamelCase/swagger.g.json @@ -51,7 +51,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/supermarketPostRequestDocument" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketPostRequestDocument" + } + ] } } } @@ -152,7 +156,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/supermarketPatchRequestDocument" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketPatchRequestDocument" + } + ] } } } @@ -335,7 +343,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/nullableToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneStaffMemberInRequest" + } + ] } } } @@ -486,7 +498,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/toManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/toManyStaffMemberInRequest" + } + ] } } } @@ -517,7 +533,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/toManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/toManyStaffMemberInRequest" + } + ] } } } @@ -548,7 +568,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/toManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/toManyStaffMemberInRequest" + } + ] } } } @@ -699,7 +723,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/toOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/toOneStaffMemberInRequest" + } + ] } } } @@ -734,7 +762,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -872,29 +900,6 @@ }, "additionalProperties": false }, - "nullValue": { - "not": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "object" - }, - { - "type": "array" - } - ], - "items": { } - }, - "nullable": true - }, "nullableStaffMemberIdentifierResponseDocument": { "required": [ "data", @@ -903,24 +908,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/staffMemberIdentifier" - }, - { - "$ref": "#/components/schemas/nullValue" } - ] + ], + "nullable": true }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] } }, "additionalProperties": false @@ -933,24 +944,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/staffMemberDataInResponse" - }, - { - "$ref": "#/components/schemas/nullValue" } - ] + ], + "nullable": true }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceDocument" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] } }, "additionalProperties": false @@ -962,14 +979,12 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/staffMemberIdentifier" - }, - { - "$ref": "#/components/schemas/nullValue" } - ] + ], + "nullable": true } }, "additionalProperties": false @@ -981,26 +996,31 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/staffMemberIdentifier" - }, - { - "$ref": "#/components/schemas/nullValue" } - ] + ], + "nullable": true }, "links": { - "$ref": "#/components/schemas/linksInRelationshipObject" + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false }, "staffMemberAttributesInResponse": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -1028,13 +1048,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceCollectionDocument" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] } }, "additionalProperties": false @@ -1048,21 +1076,33 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/staffMemberResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/staffMemberResourceType" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/staffMemberAttributesInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/staffMemberAttributesInResponse" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceObject" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1075,7 +1115,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/staffMemberResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/staffMemberResourceType" + } + ] }, "id": { "minLength": 1, @@ -1099,13 +1143,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + ] } }, "additionalProperties": false @@ -1118,17 +1170,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/staffMemberIdentifier" + "allOf": [ + { + "$ref": "#/components/schemas/staffMemberIdentifier" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] } }, "additionalProperties": false @@ -1147,17 +1211,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/staffMemberDataInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/staffMemberDataInResponse" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceDocument" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] } }, "additionalProperties": false @@ -1169,7 +1245,11 @@ "type": "string" }, "kind": { - "$ref": "#/components/schemas/supermarketType" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketType" + } + ] } }, "additionalProperties": false @@ -1184,19 +1264,30 @@ "type": "string" }, "kind": { - "$ref": "#/components/schemas/supermarketType" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketType" + } + ] } }, "additionalProperties": false }, "supermarketAttributesInResponse": { + "required": [ + "nameOfCity" + ], "type": "object", "properties": { "nameOfCity": { "type": "string" }, "kind": { - "$ref": "#/components/schemas/supermarketType" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketType" + } + ] } }, "additionalProperties": false @@ -1216,13 +1307,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceCollectionDocument" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] } }, "additionalProperties": false @@ -1235,17 +1334,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/supermarketResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketResourceType" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/supermarketAttributesInPatchRequest" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketAttributesInPatchRequest" + } + ] }, "relationships": { - "$ref": "#/components/schemas/supermarketRelationshipsInPatchRequest" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketRelationshipsInPatchRequest" + } + ] } }, "additionalProperties": false @@ -1257,13 +1368,25 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/supermarketResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketResourceType" + } + ] }, "attributes": { - "$ref": "#/components/schemas/supermarketAttributesInPostRequest" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketAttributesInPostRequest" + } + ] }, "relationships": { - "$ref": "#/components/schemas/supermarketRelationshipsInPostRequest" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketRelationshipsInPostRequest" + } + ] } }, "additionalProperties": false @@ -1277,24 +1400,40 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/supermarketResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketResourceType" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/supermarketAttributesInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketAttributesInResponse" + } + ] }, "relationships": { - "$ref": "#/components/schemas/supermarketRelationshipsInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketRelationshipsInResponse" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceObject" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1306,7 +1445,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/supermarketDataInPatchRequest" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketDataInPatchRequest" + } + ] } }, "additionalProperties": false @@ -1318,7 +1461,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/supermarketDataInPostRequest" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketDataInPostRequest" + } + ] } }, "additionalProperties": false @@ -1331,17 +1478,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/supermarketDataInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/supermarketDataInResponse" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInResourceDocument" + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] } }, "additionalProperties": false @@ -1350,13 +1509,25 @@ "type": "object", "properties": { "storeManager": { - "$ref": "#/components/schemas/toOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/toOneStaffMemberInRequest" + } + ] }, "backupStoreManager": { - "$ref": "#/components/schemas/nullableToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneStaffMemberInRequest" + } + ] }, "cashiers": { - "$ref": "#/components/schemas/toManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/toManyStaffMemberInRequest" + } + ] } }, "additionalProperties": false @@ -1368,28 +1539,55 @@ "type": "object", "properties": { "storeManager": { - "$ref": "#/components/schemas/toOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/toOneStaffMemberInRequest" + } + ] }, "backupStoreManager": { - "$ref": "#/components/schemas/nullableToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneStaffMemberInRequest" + } + ] }, "cashiers": { - "$ref": "#/components/schemas/toManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/toManyStaffMemberInRequest" + } + ] } }, "additionalProperties": false }, "supermarketRelationshipsInResponse": { + "required": [ + "storeManager" + ], "type": "object", "properties": { "storeManager": { - "$ref": "#/components/schemas/toOneStaffMemberInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/toOneStaffMemberInResponse" + } + ] }, "backupStoreManager": { - "$ref": "#/components/schemas/nullableToOneStaffMemberInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneStaffMemberInResponse" + } + ] }, "cashiers": { - "$ref": "#/components/schemas/toManyStaffMemberInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/toManyStaffMemberInResponse" + } + ] } }, "additionalProperties": false @@ -1436,11 +1634,15 @@ } }, "links": { - "$ref": "#/components/schemas/linksInRelationshipObject" + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1452,7 +1654,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/staffMemberIdentifier" + "allOf": [ + { + "$ref": "#/components/schemas/staffMemberIdentifier" + } + ] } }, "additionalProperties": false @@ -1464,18 +1670,26 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/staffMemberIdentifier" + "allOf": [ + { + "$ref": "#/components/schemas/staffMemberIdentifier" + } + ] }, "links": { - "$ref": "#/components/schemas/linksInRelationshipObject" + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false } } } -} \ No newline at end of file +} diff --git a/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json index 1d7feb51f0..1e257a710f 100644 --- a/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/KebabCase/swagger.g.json @@ -51,7 +51,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/supermarket-post-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-post-request-document" + } + ] } } } @@ -152,7 +156,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/supermarket-patch-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-patch-request-document" + } + ] } } } @@ -335,7 +343,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + } + ] } } } @@ -486,7 +498,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + ] } } } @@ -517,7 +533,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + ] } } } @@ -548,7 +568,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + ] } } } @@ -699,7 +723,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-staff-member-in-request" + } + ] } } } @@ -734,7 +762,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -872,29 +900,6 @@ }, "additionalProperties": false }, - "null-value": { - "not": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "object" - }, - { - "type": "array" - } - ], - "items": { } - }, - "nullable": true - }, "nullable-staff-member-identifier-response-document": { "required": [ "data", @@ -903,24 +908,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/staff-member-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + } + ] } }, "additionalProperties": false @@ -933,24 +944,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/staff-member-data-in-response" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -962,14 +979,12 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/staff-member-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true } }, "additionalProperties": false @@ -981,26 +996,31 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/staff-member-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false }, "staff-member-attributes-in-response": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -1028,13 +1048,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -1048,21 +1076,33 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/staff-member-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/staff-member-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/staff-member-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/staff-member-attributes-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1075,7 +1115,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/staff-member-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/staff-member-resource-type" + } + ] }, "id": { "minLength": 1, @@ -1099,13 +1143,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + } + ] } }, "additionalProperties": false @@ -1118,17 +1170,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/staff-member-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/staff-member-identifier" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + } + ] } }, "additionalProperties": false @@ -1147,17 +1211,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/staff-member-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/staff-member-data-in-response" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -1169,7 +1245,11 @@ "type": "string" }, "kind": { - "$ref": "#/components/schemas/supermarket-type" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-type" + } + ] } }, "additionalProperties": false @@ -1184,19 +1264,30 @@ "type": "string" }, "kind": { - "$ref": "#/components/schemas/supermarket-type" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-type" + } + ] } }, "additionalProperties": false }, "supermarket-attributes-in-response": { + "required": [ + "name-of-city" + ], "type": "object", "properties": { "name-of-city": { "type": "string" }, "kind": { - "$ref": "#/components/schemas/supermarket-type" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-type" + } + ] } }, "additionalProperties": false @@ -1216,13 +1307,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -1235,17 +1334,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/supermarket-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/supermarket-attributes-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-attributes-in-patch-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/supermarket-relationships-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-relationships-in-patch-request" + } + ] } }, "additionalProperties": false @@ -1257,13 +1368,25 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/supermarket-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-resource-type" + } + ] }, "attributes": { - "$ref": "#/components/schemas/supermarket-attributes-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-attributes-in-post-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/supermarket-relationships-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-relationships-in-post-request" + } + ] } }, "additionalProperties": false @@ -1277,24 +1400,40 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/supermarket-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/supermarket-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-attributes-in-response" + } + ] }, "relationships": { - "$ref": "#/components/schemas/supermarket-relationships-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-relationships-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1306,7 +1445,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/supermarket-data-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-data-in-patch-request" + } + ] } }, "additionalProperties": false @@ -1318,7 +1461,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/supermarket-data-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-data-in-post-request" + } + ] } }, "additionalProperties": false @@ -1331,17 +1478,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/supermarket-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/supermarket-data-in-response" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -1350,13 +1509,25 @@ "type": "object", "properties": { "store-manager": { - "$ref": "#/components/schemas/to-one-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-staff-member-in-request" + } + ] }, "backup-store-manager": { - "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + } + ] }, "cashiers": { - "$ref": "#/components/schemas/to-many-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + ] } }, "additionalProperties": false @@ -1368,28 +1539,55 @@ "type": "object", "properties": { "store-manager": { - "$ref": "#/components/schemas/to-one-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-staff-member-in-request" + } + ] }, "backup-store-manager": { - "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-staff-member-in-request" + } + ] }, "cashiers": { - "$ref": "#/components/schemas/to-many-staff-member-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-staff-member-in-request" + } + ] } }, "additionalProperties": false }, "supermarket-relationships-in-response": { + "required": [ + "store-manager" + ], "type": "object", "properties": { "store-manager": { - "$ref": "#/components/schemas/to-one-staff-member-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-staff-member-in-response" + } + ] }, "backup-store-manager": { - "$ref": "#/components/schemas/nullable-to-one-staff-member-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-staff-member-in-response" + } + ] }, "cashiers": { - "$ref": "#/components/schemas/to-many-staff-member-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-staff-member-in-response" + } + ] } }, "additionalProperties": false @@ -1436,11 +1634,15 @@ } }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1452,7 +1654,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/staff-member-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/staff-member-identifier" + } + ] } }, "additionalProperties": false @@ -1464,18 +1670,26 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/staff-member-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/staff-member-identifier" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false } } } -} \ No newline at end of file +} diff --git a/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json b/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json index fa5041afad..f6de5bef79 100644 --- a/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json +++ b/test/OpenApiClientTests/NamingConventions/PascalCase/swagger.g.json @@ -51,7 +51,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/SupermarketPostRequestDocument" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketPostRequestDocument" + } + ] } } } @@ -152,7 +156,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/SupermarketPatchRequestDocument" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketPatchRequestDocument" + } + ] } } } @@ -335,7 +343,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/NullableToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/NullableToOneStaffMemberInRequest" + } + ] } } } @@ -486,7 +498,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + } + ] } } } @@ -517,7 +533,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + } + ] } } } @@ -548,7 +568,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + } + ] } } } @@ -699,7 +723,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/ToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/ToOneStaffMemberInRequest" + } + ] } } } @@ -734,7 +762,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -872,29 +900,6 @@ }, "additionalProperties": false }, - "NullValue": { - "not": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "object" - }, - { - "type": "array" - } - ], - "items": { } - }, - "nullable": true - }, "NullableStaffMemberIdentifierResponseDocument": { "required": [ "data", @@ -903,24 +908,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/StaffMemberIdentifier" - }, - { - "$ref": "#/components/schemas/NullValue" } - ] + ], + "nullable": true }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/JsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/JsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceIdentifierDocument" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceIdentifierDocument" + } + ] } }, "additionalProperties": false @@ -933,24 +944,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/StaffMemberDataInResponse" - }, - { - "$ref": "#/components/schemas/NullValue" } - ] + ], + "nullable": true }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/JsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/JsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceDocument" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceDocument" + } + ] } }, "additionalProperties": false @@ -962,14 +979,12 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/StaffMemberIdentifier" - }, - { - "$ref": "#/components/schemas/NullValue" } - ] + ], + "nullable": true } }, "additionalProperties": false @@ -981,26 +996,31 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/StaffMemberIdentifier" - }, - { - "$ref": "#/components/schemas/NullValue" } - ] + ], + "nullable": true }, "links": { - "$ref": "#/components/schemas/LinksInRelationshipObject" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInRelationshipObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false }, "StaffMemberAttributesInResponse": { + "required": [ + "Name" + ], "type": "object", "properties": { "Name": { @@ -1028,13 +1048,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/JsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/JsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceCollectionDocument" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceCollectionDocument" + } + ] } }, "additionalProperties": false @@ -1048,21 +1076,33 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/StaffMemberResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/StaffMemberResourceType" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/StaffMemberAttributesInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/StaffMemberAttributesInResponse" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceObject" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1075,7 +1115,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/StaffMemberResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/StaffMemberResourceType" + } + ] }, "id": { "minLength": 1, @@ -1099,13 +1143,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/JsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/JsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceIdentifierCollectionDocument" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceIdentifierCollectionDocument" + } + ] } }, "additionalProperties": false @@ -1118,17 +1170,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/StaffMemberIdentifier" + "allOf": [ + { + "$ref": "#/components/schemas/StaffMemberIdentifier" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/JsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/JsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceIdentifierDocument" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceIdentifierDocument" + } + ] } }, "additionalProperties": false @@ -1147,17 +1211,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/StaffMemberDataInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/StaffMemberDataInResponse" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/JsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/JsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceDocument" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceDocument" + } + ] } }, "additionalProperties": false @@ -1169,7 +1245,11 @@ "type": "string" }, "Kind": { - "$ref": "#/components/schemas/SupermarketType" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketType" + } + ] } }, "additionalProperties": false @@ -1184,19 +1264,30 @@ "type": "string" }, "Kind": { - "$ref": "#/components/schemas/SupermarketType" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketType" + } + ] } }, "additionalProperties": false }, "SupermarketAttributesInResponse": { + "required": [ + "NameOfCity" + ], "type": "object", "properties": { "NameOfCity": { "type": "string" }, "Kind": { - "$ref": "#/components/schemas/SupermarketType" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketType" + } + ] } }, "additionalProperties": false @@ -1216,13 +1307,21 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/JsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/JsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceCollectionDocument" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceCollectionDocument" + } + ] } }, "additionalProperties": false @@ -1235,17 +1334,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/SupermarketResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketResourceType" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/SupermarketAttributesInPatchRequest" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketAttributesInPatchRequest" + } + ] }, "relationships": { - "$ref": "#/components/schemas/SupermarketRelationshipsInPatchRequest" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketRelationshipsInPatchRequest" + } + ] } }, "additionalProperties": false @@ -1257,13 +1368,25 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/SupermarketResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketResourceType" + } + ] }, "attributes": { - "$ref": "#/components/schemas/SupermarketAttributesInPostRequest" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketAttributesInPostRequest" + } + ] }, "relationships": { - "$ref": "#/components/schemas/SupermarketRelationshipsInPostRequest" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketRelationshipsInPostRequest" + } + ] } }, "additionalProperties": false @@ -1277,24 +1400,40 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/SupermarketResourceType" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketResourceType" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/SupermarketAttributesInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketAttributesInResponse" + } + ] }, "relationships": { - "$ref": "#/components/schemas/SupermarketRelationshipsInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketRelationshipsInResponse" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceObject" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1306,7 +1445,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SupermarketDataInPatchRequest" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketDataInPatchRequest" + } + ] } }, "additionalProperties": false @@ -1318,7 +1461,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SupermarketDataInPostRequest" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketDataInPostRequest" + } + ] } }, "additionalProperties": false @@ -1331,17 +1478,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/SupermarketDataInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/SupermarketDataInResponse" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/JsonapiObject" + "allOf": [ + { + "$ref": "#/components/schemas/JsonapiObject" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInResourceDocument" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInResourceDocument" + } + ] } }, "additionalProperties": false @@ -1350,13 +1509,25 @@ "type": "object", "properties": { "StoreManager": { - "$ref": "#/components/schemas/ToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/ToOneStaffMemberInRequest" + } + ] }, "BackupStoreManager": { - "$ref": "#/components/schemas/NullableToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/NullableToOneStaffMemberInRequest" + } + ] }, "Cashiers": { - "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + } + ] } }, "additionalProperties": false @@ -1368,28 +1539,55 @@ "type": "object", "properties": { "StoreManager": { - "$ref": "#/components/schemas/ToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/ToOneStaffMemberInRequest" + } + ] }, "BackupStoreManager": { - "$ref": "#/components/schemas/NullableToOneStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/NullableToOneStaffMemberInRequest" + } + ] }, "Cashiers": { - "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + "allOf": [ + { + "$ref": "#/components/schemas/ToManyStaffMemberInRequest" + } + ] } }, "additionalProperties": false }, "SupermarketRelationshipsInResponse": { + "required": [ + "StoreManager" + ], "type": "object", "properties": { "StoreManager": { - "$ref": "#/components/schemas/ToOneStaffMemberInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/ToOneStaffMemberInResponse" + } + ] }, "BackupStoreManager": { - "$ref": "#/components/schemas/NullableToOneStaffMemberInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/NullableToOneStaffMemberInResponse" + } + ] }, "Cashiers": { - "$ref": "#/components/schemas/ToManyStaffMemberInResponse" + "allOf": [ + { + "$ref": "#/components/schemas/ToManyStaffMemberInResponse" + } + ] } }, "additionalProperties": false @@ -1436,11 +1634,15 @@ } }, "links": { - "$ref": "#/components/schemas/LinksInRelationshipObject" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInRelationshipObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1452,7 +1654,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/StaffMemberIdentifier" + "allOf": [ + { + "$ref": "#/components/schemas/StaffMemberIdentifier" + } + ] } }, "additionalProperties": false @@ -1464,18 +1670,26 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/StaffMemberIdentifier" + "allOf": [ + { + "$ref": "#/components/schemas/StaffMemberIdentifier" + } + ] }, "links": { - "$ref": "#/components/schemas/LinksInRelationshipObject" + "allOf": [ + { + "$ref": "#/components/schemas/LinksInRelationshipObject" + } + ] }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false } } } -} \ No newline at end of file +} diff --git a/test/OpenApiClientTests/ObjectExtensions.cs b/test/OpenApiClientTests/ObjectExtensions.cs new file mode 100644 index 0000000000..9bf15b6ac7 --- /dev/null +++ b/test/OpenApiClientTests/ObjectExtensions.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using JsonApiDotNetCore.OpenApi.Client; + +namespace OpenApiClientTests; + +internal static class ObjectExtensions +{ + public static object? GetPropertyValue(this object source, string propertyName) + { + ArgumentGuard.NotNull(source); + ArgumentGuard.NotNull(propertyName); + + PropertyInfo propertyInfo = source.GetType().GetProperties().Single(property => property.Name == propertyName); + + return propertyInfo.GetValue(source); + } + + public static void SetPropertyValue(this object source, string propertyName, object? value) + { + ArgumentGuard.NotNull(source); + ArgumentGuard.NotNull(propertyName); + + PropertyInfo propertyInfo = source.GetType().GetProperties().Single(property => property.Name == propertyName); + + propertyInfo.SetValue(source, value); + } + + public static object? GetDefaultValueForProperty(this object source, string propertyName) + { + ArgumentGuard.NotNull(source); + ArgumentGuard.NotNull(propertyName); + + PropertyInfo propertyInfo = source.GetType().GetProperties().Single(property => property.Name == propertyName); + + return Activator.CreateInstance(propertyInfo.PropertyType); + } +} diff --git a/test/OpenApiClientTests/OpenApiClientTests.cs b/test/OpenApiClientTests/OpenApiClientTests.cs new file mode 100644 index 0000000000..bb0b5d61a1 --- /dev/null +++ b/test/OpenApiClientTests/OpenApiClientTests.cs @@ -0,0 +1,20 @@ +using System.Linq.Expressions; + +namespace OpenApiClientTests; + +public class OpenApiClientTests +{ + private const string AttributesObjectParameterName = "attributesObject"; + + protected static Expression> CreateIncludedAttributeSelector(string propertyName) + where TAttributesObject : class + { + Type attributesObjectType = typeof(TAttributesObject); + + ParameterExpression parameter = Expression.Parameter(attributesObjectType, AttributesObjectParameterName); + MemberExpression property = Expression.Property(parameter, propertyName); + UnaryExpression toObjectConversion = Expression.Convert(property, typeof(object)); + + return Expression.Lambda>(toObjectConversion, parameter); + } +} diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj index 4d7bf979ec..c5e7f0acdd 100644 --- a/test/OpenApiClientTests/OpenApiClientTests.csproj +++ b/test/OpenApiClientTests/OpenApiClientTests.csproj @@ -9,6 +9,7 @@ + @@ -52,5 +53,34 @@ NSwagCSharp /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions + + NrtOffMsvOffClient + NrtOffMsvOffClient.cs + NSwagCSharp + OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled.GeneratedCode + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:false + + + NrtOffMsvOnClient + NrtOffMsvOnClient.cs + NSwagCSharp + OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled.GeneratedCode + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:false + + + NrtOnMsvOffClient + NrtOnMsvOffClient.cs + NSwagCSharp + OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled.GeneratedCode + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:true + + + NrtOnMsvOnClient + NrtOnMsvOnClient.cs + NSwagCSharp + OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled.GeneratedCode + /UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:true + - \ No newline at end of file + + diff --git a/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs b/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs new file mode 100644 index 0000000000..e49bef1a0d --- /dev/null +++ b/test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using FluentAssertions; +using FluentAssertions.Types; + +namespace OpenApiClientTests; + +internal static class PropertyInfoAssertionsExtensions +{ + [CustomAssertion] + public static void HaveNullabilityState(this PropertyInfoAssertions source, NullabilityState expected, string because = "", params object[] becauseArgs) + { + PropertyInfo propertyInfo = source.Subject; + + NullabilityInfoContext nullabilityContext = new(); + NullabilityInfo nullabilityInfo = nullabilityContext.Create(propertyInfo); + + nullabilityInfo.ReadState.Should().Be(expected, because, becauseArgs); + nullabilityInfo.WriteState.Should().Be(expected, because, becauseArgs); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/CreateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/CreateResourceTests.cs new file mode 100644 index 0000000000..5dfa347b05 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/CreateResourceTests.cs @@ -0,0 +1,445 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled; + +public sealed class CreateResourceTests : OpenApiClientTests +{ + private readonly ResourceFieldValidationFakers _fakers = new(); + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_clear_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(attributePropertyName); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldContainPath(jsonPropertyName).With(attribute => attribute.ValueKind.Should().Be(JsonValueKind.Null)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + public async Task Can_set_default_value_to_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? defaultValue = requestDocument.Data.Attributes.GetDefaultValueForProperty(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldContainPath(jsonPropertyName).With(attribute => attribute.ShouldBeInteger(0)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPostRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Cannot_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPostRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + InvalidOperationException exception = assertion.Subject.Single(); + exception.Message.Should().Be($"The following property should not be omitted: data.attributes.{jsonPropertyName}."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), nameof(ResourceRelationshipsInPostRequest.ToOne.Data), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), nameof(ResourceRelationshipsInPostRequest.RequiredToOne.Data), "requiredToOne")] + public async Task Can_clear_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), nameof(ResourceRelationshipsInPostRequest.ToOne.Data), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), nameof(ResourceRelationshipsInPostRequest.RequiredToOne.Data), "requiredToOne")] + public async Task Can_clear_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), nameof(ResourceRelationshipsInPostRequest.ToMany.Data), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), nameof(ResourceRelationshipsInPostRequest.RequiredToMany.Data), "requiredToMany")] + public async Task Cannot_clear_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(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.{jsonPropertyName}'."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), nameof(ResourceRelationshipsInPostRequest.ToMany.Data), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), nameof(ResourceRelationshipsInPostRequest.RequiredToMany.Data), "requiredToMany")] + public async Task Cannot_clear_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(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.{jsonPropertyName}'."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + public async Task Can_exclude_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + public async Task Can_exclude_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/GeneratedCode/NrtOffMsvOffClient.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/GeneratedCode/NrtOffMsvOffClient.cs new file mode 100644 index 0000000000..4bea9db090 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/GeneratedCode/NrtOffMsvOffClient.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.OpenApi.Client; +using Newtonsoft.Json; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled.GeneratedCode; + +internal partial class NrtOffMsvOffClient : JsonApiClient +{ + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/NullabilityTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/NullabilityTests.cs new file mode 100644 index 0000000000..8079c4896c --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/NullabilityTests.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using FluentAssertions; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled.GeneratedCode; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled; + +public sealed class NullabilityTests +{ + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), NullabilityState.Unknown)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), NullabilityState.Unknown)] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), NullabilityState.Nullable)] + public void Nullability_of_generated_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + PropertyInfo[] properties = typeof(ResourceAttributesInPostRequest).GetProperties(); + PropertyInfo property = properties.Single(property => property.Name == propertyName); + property.Should().HaveNullabilityState(expectedState); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/ResourceFieldValidationFakers.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/ResourceFieldValidationFakers.cs new file mode 100644 index 0000000000..817344c11c --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/ResourceFieldValidationFakers.cs @@ -0,0 +1,24 @@ +using Bogus; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled.GeneratedCode; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled; + +internal sealed class ResourceFieldValidationFakers +{ + private readonly Lazy> _lazyPostAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyPatchAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyNullableToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyToManyFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + public Faker PostAttributes => _lazyPostAttributesFaker.Value; + public Faker PatchAttributes => _lazyPatchAttributesFaker.Value; + public Faker NullableToOne => _lazyNullableToOneFaker.Value; + public Faker ToMany => _lazyToManyFaker.Value; +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/UpdateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/UpdateResourceTests.cs new file mode 100644 index 0000000000..68e71e97b9 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/UpdateResourceTests.cs @@ -0,0 +1,146 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled; + +public sealed class UpdateResourceTests +{ + private readonly ResourceFieldValidationFakers _fakers = new(); + + [Fact] + public async Task Cannot_exclude_id() + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceDataInPatchRequest emptyDataObject = new(); + requestDocument.Data.Id = emptyDataObject.Id; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + // Act + Func action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(999, 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'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPatchRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPatchRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToOne), "requiredToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToMany), "requiredToMany")] + public async Task Can_exclude_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPatchRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/swagger.g.json b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/swagger.g.json new file mode 100644 index 0000000000..27d5a781d9 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/swagger.g.json @@ -0,0 +1,1854 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/Resource": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResource", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/resourcePostRequestDocument" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResource", + "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/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResource", + "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/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/resourcePatchRequestDocument" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredToMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredToMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/toMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/toMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/toOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/toOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "emptyResourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceResourceType": { + "enum": [ + "emptyResources" + ], + "type": "string" + }, + "jsonapiObject": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "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" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "nullableEmptyResourceIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] + } + }, + "additionalProperties": false + }, + "nullableEmptyResourceSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "resourceAttributesInPatchRequest": { + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceAttributesInPostRequest": { + "required": [ + "requiredNullableValueType", + "requiredReferenceType", + "requiredValueType" + ], + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceAttributesInResponse": { + "required": [ + "requiredNullableValueType", + "requiredReferenceType", + "requiredValueType" + ], + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInPatchRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInPatchRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInPostRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInResponse" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInResponse" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "resourcePatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInPatchRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourcePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourcePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInResponse" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "toOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "requiredToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPostRequest": { + "required": [ + "requiredToMany", + "requiredToOne" + ], + "type": "object", + "properties": { + "toOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "requiredToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInResponse": { + "required": [ + "requiredToMany", + "requiredToOne" + ], + "type": "object", + "properties": { + "toOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInResponse" + } + ] + }, + "requiredToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInResponse" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInResponse" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInResponse" + } + ] + } + }, + "additionalProperties": false + }, + "resourceResourceType": { + "enum": [ + "Resource" + ], + "type": "string" + }, + "toManyEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + } + } + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/CreateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/CreateResourceTests.cs new file mode 100644 index 0000000000..f3a5130486 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/CreateResourceTests.cs @@ -0,0 +1,569 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled; + +public sealed class CreateResourceTests : OpenApiClientTests +{ + private readonly ResourceFieldValidationFakers _fakers = new(); + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_clear_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(attributePropertyName); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldContainPath(jsonPropertyName).With(attribute => attribute.ValueKind.Should().Be(JsonValueKind.Null)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_set_default_value_to_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? defaultValue = requestDocument.Data.Attributes.GetDefaultValueForProperty(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldContainPath(jsonPropertyName).With(attribute => attribute.ShouldBeInteger(0)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), "requiredReferenceType")] + public async Task Cannot_clear_attribute(string clrPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + requestDocument.Data.Attributes.SetPropertyValue(clrPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(clrPropertyName); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Contain($"Cannot write a null value for property '{jsonPropertyName}'."); + } + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPostRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Cannot_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPostRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + InvalidOperationException exception = assertion.Subject.Single(); + exception.Message.Should().Be($"The following property should not be omitted: data.attributes.{jsonPropertyName}."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), nameof(ResourceRelationshipsInPostRequest.ToOne.Data), "toOne")] + public async Task Can_clear_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), nameof(ResourceRelationshipsInPostRequest.ToOne.Data), "toOne")] + public async Task Can_clear_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), nameof(ResourceRelationshipsInPostRequest.RequiredToOne.Data), "requiredToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), nameof(ResourceRelationshipsInPostRequest.ToMany.Data), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), nameof(ResourceRelationshipsInPostRequest.RequiredToMany.Data), "requiredToMany")] + public async Task Cannot_clear_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(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.{jsonPropertyName}'."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), nameof(ResourceRelationshipsInPostRequest.RequiredToOne.Data), "requiredToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), nameof(ResourceRelationshipsInPostRequest.ToMany.Data), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), nameof(ResourceRelationshipsInPostRequest.RequiredToMany.Data), "requiredToMany")] + public async Task Cannot_clear_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(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.{jsonPropertyName}'."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Can_exclude_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Can_exclude_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), "requiredToOne")] + public async Task Cannot_exclude_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + 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.relationships.{jsonPropertyName}.data'."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToOne), "requiredToOne")] + public async Task Cannot_exclude_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + 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.relationships.{jsonPropertyName}.data'."); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/GeneratedCode/NrtOffMsvOnClient.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/GeneratedCode/NrtOffMsvOnClient.cs new file mode 100644 index 0000000000..94bb5efc37 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/GeneratedCode/NrtOffMsvOnClient.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.OpenApi.Client; +using Newtonsoft.Json; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled.GeneratedCode; + +internal partial class NrtOffMsvOnClient : JsonApiClient +{ + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/NullabilityTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/NullabilityTests.cs new file mode 100644 index 0000000000..53252ee439 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/NullabilityTests.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using FluentAssertions; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled.GeneratedCode; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled; + +public sealed class NullabilityTests +{ + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ReferenceType), NullabilityState.Unknown)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredReferenceType), NullabilityState.Unknown)] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), NullabilityState.NotNull)] + public void Nullability_of_generated_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + PropertyInfo[] properties = typeof(ResourceAttributesInPostRequest).GetProperties(); + PropertyInfo property = properties.Single(property => property.Name == propertyName); + property.Should().HaveNullabilityState(expectedState); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/ResourceFieldValidationFakers.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/ResourceFieldValidationFakers.cs new file mode 100644 index 0000000000..e3dbbd0d36 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/ResourceFieldValidationFakers.cs @@ -0,0 +1,28 @@ +using Bogus; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled.GeneratedCode; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled; + +internal sealed class ResourceFieldValidationFakers +{ + private readonly Lazy> _lazyPostAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyPatchAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyNullableToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyToManyFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + public Faker PostAttributes => _lazyPostAttributesFaker.Value; + public Faker PatchAttributes => _lazyPatchAttributesFaker.Value; + public Faker ToOne => _lazyToOneFaker.Value; + public Faker NullableToOne => _lazyNullableToOneFaker.Value; + public Faker ToMany => _lazyToManyFaker.Value; +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/UpdateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/UpdateResourceTests.cs new file mode 100644 index 0000000000..81f54f6dd0 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/UpdateResourceTests.cs @@ -0,0 +1,146 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled; + +public sealed class UpdateResourceTests +{ + private readonly ResourceFieldValidationFakers _fakers = new(); + + [Fact] + public async Task Cannot_exclude_id() + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceDataInPatchRequest emptyDataObject = new(); + requestDocument.Data.Id = emptyDataObject.Id; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + // Act + Func action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(999, 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'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPatchRequest.ReferenceType), "referenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredReferenceType), "requiredReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPatchRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToOne), "toOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToOne), "requiredToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToMany), "requiredToMany")] + public async Task Can_exclude_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + ToOne = _fakers.NullableToOne.Generate(), + RequiredToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPatchRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOffMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/swagger.g.json b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/swagger.g.json new file mode 100644 index 0000000000..56e047eaee --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/swagger.g.json @@ -0,0 +1,1957 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/Resource": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResource", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/resourcePostRequestDocument" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResource", + "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/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResource", + "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/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/resourcePatchRequestDocument" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredToMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredToMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/toMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/toMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/toOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/toOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "emptyResourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceResourceType": { + "enum": [ + "emptyResources" + ], + "type": "string" + }, + "emptyResourceSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "jsonapiObject": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "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" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "nullableEmptyResourceIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] + } + }, + "additionalProperties": false + }, + "nullableEmptyResourceSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "resourceAttributesInPatchRequest": { + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceAttributesInPostRequest": { + "required": [ + "requiredNullableValueType", + "requiredReferenceType" + ], + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceAttributesInResponse": { + "required": [ + "requiredNullableValueType", + "requiredReferenceType" + ], + "type": "object", + "properties": { + "referenceType": { + "type": "string", + "nullable": true + }, + "requiredReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInPatchRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInPatchRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInPostRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInResponse" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInResponse" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "resourcePatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInPatchRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourcePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourcePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInResponse" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "toOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "requiredToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPostRequest": { + "required": [ + "requiredToOne" + ], + "type": "object", + "properties": { + "toOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "requiredToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInResponse": { + "required": [ + "requiredToOne" + ], + "type": "object", + "properties": { + "toOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInResponse" + } + ] + }, + "requiredToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInResponse" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInResponse" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInResponse" + } + ] + } + }, + "additionalProperties": false + }, + "resourceResourceType": { + "enum": [ + "Resource" + ], + "type": "string" + }, + "toManyEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "toOneEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + } + }, + "additionalProperties": false + }, + "toOneEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + } + } + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/CreateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/CreateResourceTests.cs new file mode 100644 index 0000000000..0ca3a6fb82 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/CreateResourceTests.cs @@ -0,0 +1,609 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled; + +public sealed class CreateResourceTests : OpenApiClientTests +{ + private readonly ResourceFieldValidationFakers _fakers = new(); + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_clear_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(attributePropertyName); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldContainPath(jsonPropertyName).With(attribute => attribute.ValueKind.Should().Be(JsonValueKind.Null)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + public async Task Can_set_default_value_to_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? defaultValue = requestDocument.Data.Attributes.GetDefaultValueForProperty(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldContainPath(jsonPropertyName).With(attribute => attribute.ShouldBeInteger(0)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + public async Task Cannot_clear_attribute(string clrPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + requestDocument.Data.Attributes.SetPropertyValue(clrPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(clrPropertyName); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Contain($"Cannot write a null value for property '{jsonPropertyName}'."); + } + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPostRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Cannot_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPostRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + InvalidOperationException exception = assertion.Subject.Single(); + exception.Message.Should().Be($"The following property should not be omitted: data.attributes.{jsonPropertyName}."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), nameof(ResourceRelationshipsInPostRequest.NullableToOne.Data), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne.Data), + "requiredNullableToOne")] + public async Task Can_clear_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), nameof(ResourceRelationshipsInPostRequest.NullableToOne.Data), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne.Data), + "requiredNullableToOne")] + public async Task Can_clear_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), nameof(ResourceRelationshipsInPostRequest.NonNullableToOne.Data), + "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne.Data), + "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), nameof(ResourceRelationshipsInPostRequest.ToMany.Data), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), nameof(ResourceRelationshipsInPostRequest.RequiredToMany.Data), "requiredToMany")] + public async Task Cannot_clear_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(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.{jsonPropertyName}'."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), nameof(ResourceRelationshipsInPostRequest.NonNullableToOne.Data), + "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne.Data), + "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), nameof(ResourceRelationshipsInPostRequest.ToMany.Data), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), nameof(ResourceRelationshipsInPostRequest.RequiredToMany.Data), "requiredToMany")] + public async Task Cannot_clear_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(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.{jsonPropertyName}'."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + public async Task Can_exclude_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + public async Task Can_exclude_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + public async Task Cannot_exclude_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + 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.relationships.{jsonPropertyName}.data'."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + public async Task Cannot_exclude_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + 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.relationships.{jsonPropertyName}.data'."); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/GeneratedCode/NrtOnMsvOffClient.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/GeneratedCode/NrtOnMsvOffClient.cs new file mode 100644 index 0000000000..c44cdbe3d7 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/GeneratedCode/NrtOnMsvOffClient.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.OpenApi.Client; +using Newtonsoft.Json; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled.GeneratedCode; + +internal partial class NrtOnMsvOffClient : JsonApiClient +{ + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/NullabilityTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/NullabilityTests.cs new file mode 100644 index 0000000000..dc2bc84ccd --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/NullabilityTests.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using FluentAssertions; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled.GeneratedCode; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled; + +public sealed class NullabilityTests +{ + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), NullabilityState.Nullable)] + public void Nullability_of_generated_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + PropertyInfo[] properties = typeof(ResourceAttributesInPostRequest).GetProperties(); + PropertyInfo property = properties.Single(property => property.Name == propertyName); + property.Should().HaveNullabilityState(expectedState); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/ResourceFieldValidationFakers.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/ResourceFieldValidationFakers.cs new file mode 100644 index 0000000000..b0c9c260c7 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/ResourceFieldValidationFakers.cs @@ -0,0 +1,28 @@ +using Bogus; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled.GeneratedCode; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled; + +internal sealed class ResourceFieldValidationFakers +{ + private readonly Lazy> _lazyPostAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyPatchAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyNullableToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyToManyFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + public Faker PostAttributes => _lazyPostAttributesFaker.Value; + public Faker PatchAttributes => _lazyPatchAttributesFaker.Value; + public Faker ToOne => _lazyToOneFaker.Value; + public Faker NullableToOne => _lazyNullableToOneFaker.Value; + public Faker ToMany => _lazyToManyFaker.Value; +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/UpdateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/UpdateResourceTests.cs new file mode 100644 index 0000000000..5596c17962 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/UpdateResourceTests.cs @@ -0,0 +1,156 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled; + +public sealed class UpdateResourceTests +{ + private readonly ResourceFieldValidationFakers _fakers = new(); + + [Fact] + public async Task Cannot_exclude_id() + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceDataInPatchRequest emptyDataObject = new(); + requestDocument.Data.Id = emptyDataObject.Id; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + // Act + Func action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(999, 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'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPatchRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_exclude_attribute_that_is_required_in_create_resource(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPatchRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredNullableToOne), "requiredNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToMany), "requiredToMany")] + public async Task Can_exclude_relationship_that_is_required_in_create_resource(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.NullableToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPatchRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOffClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/swagger.g.json b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/swagger.g.json new file mode 100644 index 0000000000..71ebea9da7 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/swagger.g.json @@ -0,0 +1,2344 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/Resource": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResource", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/resourcePostRequestDocument" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResource", + "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/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResource", + "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/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/resourcePatchRequestDocument" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/nonNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceNonNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceNonNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/nonNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceNonNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceNonNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/nullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceNullableToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceNullableToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/nullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceNullableToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceNullableToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredNonNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredNonNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredNonNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredNonNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredNonNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredNonNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredNullableToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredNullableToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredNullableToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredNullableToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredToMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredToMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/toMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/toMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "emptyResourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceResourceType": { + "enum": [ + "emptyResources" + ], + "type": "string" + }, + "emptyResourceSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "jsonapiObject": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "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" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "nullableEmptyResourceIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] + } + }, + "additionalProperties": false + }, + "nullableEmptyResourceSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "resourceAttributesInPatchRequest": { + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceAttributesInPostRequest": { + "required": [ + "requiredNonNullableReferenceType", + "requiredNullableReferenceType", + "requiredNullableValueType", + "requiredValueType" + ], + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceAttributesInResponse": { + "required": [ + "requiredNonNullableReferenceType", + "requiredNullableReferenceType", + "requiredNullableValueType", + "requiredValueType" + ], + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string", + "nullable": true + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + } + }, + "additionalProperties": false + }, + "resourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInPatchRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInPatchRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInPostRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInResponse" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInResponse" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "resourcePatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInPatchRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourcePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourcePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInResponse" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "nonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "requiredNonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "nullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "requiredNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPostRequest": { + "required": [ + "requiredNonNullableToOne", + "requiredNullableToOne", + "requiredToMany" + ], + "type": "object", + "properties": { + "nonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "requiredNonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "nullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "requiredNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInResponse": { + "required": [ + "requiredNonNullableToOne", + "requiredNullableToOne", + "requiredToMany" + ], + "type": "object", + "properties": { + "nonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInResponse" + } + ] + }, + "requiredNonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInResponse" + } + ] + }, + "nullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInResponse" + } + ] + }, + "requiredNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInResponse" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInResponse" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInResponse" + } + ] + } + }, + "additionalProperties": false + }, + "resourceResourceType": { + "enum": [ + "Resource" + ], + "type": "string" + }, + "toManyEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "toOneEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + } + }, + "additionalProperties": false + }, + "toOneEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + } + } + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/CreateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/CreateResourceTests.cs new file mode 100644 index 0000000000..44aa5da920 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/CreateResourceTests.cs @@ -0,0 +1,613 @@ +using System.Linq.Expressions; +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled; + +public sealed class CreateResourceTests : OpenApiClientTests +{ + private readonly ResourceFieldValidationFakers _fakers = new(); + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_clear_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(attributePropertyName); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldContainPath(jsonPropertyName).With(attribute => attribute.ValueKind.Should().Be(JsonValueKind.Null)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_set_default_value_to_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? defaultValue = requestDocument.Data.Attributes.GetDefaultValueForProperty(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(attributePropertyName); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldContainPath(jsonPropertyName).With(attribute => attribute.ShouldBeInteger(0)); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + public async Task Cannot_clear_attribute(string clrPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + requestDocument.Data.Attributes.SetPropertyValue(clrPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + Expression> includeAttributeSelector = + CreateIncludedAttributeSelector(clrPropertyName); + + using (apiClient.WithPartialAttributeSerialization(requestDocument, includeAttributeSelector)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + JsonSerializationException exception = assertion.Subject.Single(); + + exception.Message.Should().Contain($"Cannot write a null value for property '{jsonPropertyName}'."); + } + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), "nullableValueType")] + public async Task Can_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPostRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Cannot_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPostRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + InvalidOperationException exception = assertion.Subject.Single(); + exception.Message.Should().Be($"The following property should not be omitted: data.attributes.{jsonPropertyName}."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), nameof(ResourceRelationshipsInPostRequest.NullableToOne.Data), "nullableToOne")] + public async Task Can_clear_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), nameof(ResourceRelationshipsInPostRequest.NullableToOne.Data), "nullableToOne")] + public async Task Can_clear_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath($"data.relationships.{jsonPropertyName}.data").With(relationshipDataObject => + { + relationshipDataObject.ValueKind.Should().Be(JsonValueKind.Null); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), nameof(ResourceRelationshipsInPostRequest.NonNullableToOne.Data), + "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne.Data), + "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne.Data), + "requiredNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), nameof(ResourceRelationshipsInPostRequest.ToMany.Data), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), nameof(ResourceRelationshipsInPostRequest.RequiredToMany.Data), "requiredToMany")] + public async Task Cannot_clear_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(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.{jsonPropertyName}'."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), nameof(ResourceRelationshipsInPostRequest.NonNullableToOne.Data), + "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne.Data), + "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne.Data), + "requiredNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), nameof(ResourceRelationshipsInPostRequest.ToMany.Data), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), nameof(ResourceRelationshipsInPostRequest.RequiredToMany.Data), "requiredToMany")] + public async Task Cannot_clear_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string dataPropertyName, + string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + object? relationshipObject = requestDocument.Data.Relationships.GetPropertyValue(relationshipPropertyName); + relationshipObject!.SetPropertyValue(dataPropertyName, null); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(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.{jsonPropertyName}'."); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Can_exclude_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredToMany), "requiredToMany")] + public async Task Can_exclude_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), "requiredNullableToOne")] + public async Task Cannot_exclude_relationship_with_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + 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.relationships.{jsonPropertyName}.data'."); + } + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPostRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPostRequest.RequiredNullableToOne), "requiredNullableToOne")] + public async Task Cannot_exclude_relationship_without_partial_attribute_serialization(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePostRequestDocument + { + Data = new ResourceDataInPostRequest + { + Attributes = _fakers.PostAttributes.Generate(), + Relationships = new ResourceRelationshipsInPostRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPostRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + Func> action = async () => + await ApiResponse.TranslateAsync(async () => await apiClient.PostResourceAsync(requestDocument)); + + // Assert + 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.relationships.{jsonPropertyName}.data'."); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/GeneratedCode/NrtOnMsvOnClient.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/GeneratedCode/NrtOnMsvOnClient.cs new file mode 100644 index 0000000000..1eed829acb --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/GeneratedCode/NrtOnMsvOnClient.cs @@ -0,0 +1,14 @@ +using JsonApiDotNetCore.OpenApi.Client; +using Newtonsoft.Json; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled.GeneratedCode; + +internal partial class NrtOnMsvOnClient : JsonApiClient +{ + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/NullabilityTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/NullabilityTests.cs new file mode 100644 index 0000000000..e3c1a5ce31 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/NullabilityTests.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using FluentAssertions; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled.GeneratedCode; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled; + +public sealed class NullabilityTests +{ + [Theory] + [InlineData(nameof(ResourceAttributesInPostRequest.NonNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNonNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableReferenceType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableReferenceType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.ValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredValueType), NullabilityState.NotNull)] + [InlineData(nameof(ResourceAttributesInPostRequest.NullableValueType), NullabilityState.Nullable)] + [InlineData(nameof(ResourceAttributesInPostRequest.RequiredNullableValueType), NullabilityState.NotNull)] + public void Nullability_of_generated_property_is_as_expected(string propertyName, NullabilityState expectedState) + { + PropertyInfo[] properties = typeof(ResourceAttributesInPostRequest).GetProperties(); + PropertyInfo property = properties.Single(property => property.Name == propertyName); + property.Should().HaveNullabilityState(expectedState); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/ResourceFieldValidationFakers.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/ResourceFieldValidationFakers.cs new file mode 100644 index 0000000000..c032f0c073 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/ResourceFieldValidationFakers.cs @@ -0,0 +1,28 @@ +using Bogus; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled.GeneratedCode; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled; + +internal sealed class ResourceFieldValidationFakers +{ + private readonly Lazy> _lazyPostAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyPatchAttributesFaker = new(() => + FakerFactory.Instance.Create()); + + private readonly Lazy> _lazyToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyNullableToOneFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + private readonly Lazy> _lazyToManyFaker = new(() => + FakerFactory.Instance.CreateForObjectWithResourceId()); + + public Faker PostAttributes => _lazyPostAttributesFaker.Value; + public Faker PatchAttributes => _lazyPatchAttributesFaker.Value; + public Faker ToOne => _lazyToOneFaker.Value; + public Faker NullableToOne => _lazyNullableToOneFaker.Value; + public Faker ToMany => _lazyToManyFaker.Value; +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/UpdateResourceTests.cs b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/UpdateResourceTests.cs new file mode 100644 index 0000000000..d0695d6cf7 --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/UpdateResourceTests.cs @@ -0,0 +1,156 @@ +using System.Net; +using System.Text.Json; +using FluentAssertions; +using FluentAssertions.Specialized; +using Newtonsoft.Json; +using OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiClientTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled; + +public sealed class UpdateResourceTests +{ + private readonly ResourceFieldValidationFakers _fakers = new(); + + [Fact] + public async Task Cannot_exclude_id() + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceDataInPatchRequest emptyDataObject = new(); + requestDocument.Data.Id = emptyDataObject.Id; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + // Act + Func action = async () => await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(999, 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'."); + } + + [Theory] + [InlineData(nameof(ResourceAttributesInPatchRequest.NonNullableReferenceType), "nonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNonNullableReferenceType), "requiredNonNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableReferenceType), "nullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableReferenceType), "requiredNullableReferenceType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.ValueType), "valueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredValueType), "requiredValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.NullableValueType), "nullableValueType")] + [InlineData(nameof(ResourceAttributesInPatchRequest.RequiredNullableValueType), "requiredNullableValueType")] + public async Task Can_exclude_attribute(string attributePropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceAttributesInPatchRequest emptyAttributesObject = new(); + object? defaultValue = emptyAttributesObject.GetPropertyValue(attributePropertyName); + requestDocument.Data.Attributes.SetPropertyValue(attributePropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.attributes").With(attributesObject => + { + attributesObject.ShouldNotContainPath(jsonPropertyName); + }); + } + + [Theory] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.NonNullableToOne), "nonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredNonNullableToOne), "requiredNonNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.NullableToOne), "nullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredNullableToOne), "requiredNullableToOne")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.ToMany), "toMany")] + [InlineData(nameof(ResourceRelationshipsInPatchRequest.RequiredToMany), "requiredToMany")] + public async Task Can_exclude_relationship(string relationshipPropertyName, string jsonPropertyName) + { + // Arrange + var requestDocument = new ResourcePatchRequestDocument + { + Data = new ResourceDataInPatchRequest + { + Id = "1", + Attributes = _fakers.PatchAttributes.Generate(), + Relationships = new ResourceRelationshipsInPatchRequest + { + NonNullableToOne = _fakers.ToOne.Generate(), + RequiredNonNullableToOne = _fakers.ToOne.Generate(), + NullableToOne = _fakers.NullableToOne.Generate(), + RequiredNullableToOne = _fakers.ToOne.Generate(), + ToMany = _fakers.ToMany.Generate(), + RequiredToMany = _fakers.ToMany.Generate() + } + } + }; + + ResourceRelationshipsInPatchRequest emptyRelationshipsObject = new(); + object? defaultValue = emptyRelationshipsObject.GetPropertyValue(relationshipPropertyName); + requestDocument.Data.Relationships.SetPropertyValue(relationshipPropertyName, defaultValue); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + var apiClient = new NrtOnMsvOnClient(wrapper.HttpClient); + + using (apiClient.WithPartialAttributeSerialization(requestDocument)) + { + // Act + await ApiResponse.TranslateAsync(async () => await apiClient.PatchResourceAsync(int.Parse(requestDocument.Data.Id), requestDocument)); + } + + // Assert + JsonElement document = wrapper.ParseRequestBody(); + + document.ShouldContainPath("data.relationships").With(relationshipsObject => + { + relationshipsObject.ShouldNotContainPath(jsonPropertyName); + }); + } +} diff --git a/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/swagger.g.json b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/swagger.g.json new file mode 100644 index 0000000000..b9565e70db --- /dev/null +++ b/test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/swagger.g.json @@ -0,0 +1,2338 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "OpenApiTests", + "version": "1.0" + }, + "paths": { + "/Resource": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceCollection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourceCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResource", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/resourcePostRequestDocument" + } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResource", + "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/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResource", + "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/resourcePrimaryResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/resourcePatchRequestDocument" + } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/resourcePrimaryResponseDocument" + } + } + } + }, + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResource", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/nonNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceNonNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceNonNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/nonNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceNonNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceNonNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/nullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceNullableToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceNullableToOne", + "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/nullableEmptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/nullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceNullableToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceNullableToOneRelationship", + "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/nullableEmptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredNonNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredNonNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredNonNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredNonNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredNonNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredNonNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredNonNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredNullableToOne", + "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/emptyResourceSecondaryResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredNullableToOne": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredNullableToOneRelationship", + "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/emptyResourceIdentifierResponseDocument" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredNullableToOneRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/requiredToMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/requiredToMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceRequiredToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceRequiredToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResourceRequiredToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + }, + "/Resource/{id}/toMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToMany", + "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/emptyResourceCollectionResponseDocument" + } + } + } + } + } + } + }, + "/Resource/{id}/relationships/toMany": { + "get": { + "tags": [ + "Resource" + ], + "operationId": "getResourceToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "head": { + "tags": [ + "Resource" + ], + "operationId": "headResourceToManyRelationship", + "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/emptyResourceIdentifierCollectionResponseDocument" + } + } + } + } + } + }, + "post": { + "tags": [ + "Resource" + ], + "operationId": "postResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "patch": { + "tags": [ + "Resource" + ], + "operationId": "patchResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + }, + "delete": { + "tags": [ + "Resource" + ], + "operationId": "deleteResourceToManyRelationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + } + } + }, + "responses": { + "204": { + "description": "No Content" + } + } + } + } + }, + "components": { + "schemas": { + "emptyResourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifierCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] + } + }, + "additionalProperties": false + }, + "emptyResourceResourceType": { + "enum": [ + "emptyResources" + ], + "type": "string" + }, + "emptyResourceSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "jsonapiObject": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "linksInRelationshipObject": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "related": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "minLength": 1, + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "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" + ], + "type": "object", + "properties": { + "self": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "nullableEmptyResourceIdentifierResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + } + ] + } + }, + "additionalProperties": false + }, + "nullableEmptyResourceSecondaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceDataInResponse" + } + ], + "nullable": true + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + } + }, + "additionalProperties": false + }, + "nullableToOneEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ], + "nullable": true + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "resourceAttributesInPatchRequest": { + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceAttributesInPostRequest": { + "required": [ + "nonNullableReferenceType", + "requiredNonNullableReferenceType", + "requiredNullableReferenceType", + "requiredNullableValueType" + ], + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceAttributesInResponse": { + "required": [ + "nonNullableReferenceType", + "requiredNonNullableReferenceType", + "requiredNullableReferenceType", + "requiredNullableValueType" + ], + "type": "object", + "properties": { + "nonNullableReferenceType": { + "type": "string" + }, + "requiredNonNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "nullableReferenceType": { + "type": "string", + "nullable": true + }, + "requiredNullableReferenceType": { + "minLength": 1, + "type": "string" + }, + "valueType": { + "type": "integer", + "format": "int32" + }, + "requiredValueType": { + "type": "integer", + "format": "int32" + }, + "nullableValueType": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "requiredNullableValueType": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "resourceCollectionResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/resourceDataInResponse" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInPatchRequest": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInPatchRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInPatchRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInPostRequest": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInPostRequest" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceDataInResponse": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceResourceType" + } + ] + }, + "id": { + "minLength": 1, + "type": "string" + }, + "attributes": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceAttributesInResponse" + } + ] + }, + "relationships": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceRelationshipsInResponse" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "resourcePatchRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInPatchRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourcePostRequestDocument": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInPostRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourcePrimaryResponseDocument": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/resourceDataInResponse" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "allOf": [ + { + "$ref": "#/components/schemas/jsonapiObject" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInResourceDocument" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPatchRequest": { + "type": "object", + "properties": { + "nonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "requiredNonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "nullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "requiredNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInPostRequest": { + "required": [ + "nonNullableToOne", + "requiredNonNullableToOne", + "requiredNullableToOne" + ], + "type": "object", + "properties": { + "nonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "requiredNonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "nullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInRequest" + } + ] + }, + "requiredNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInRequest" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInRequest" + } + ] + } + }, + "additionalProperties": false + }, + "resourceRelationshipsInResponse": { + "required": [ + "nonNullableToOne", + "requiredNonNullableToOne", + "requiredNullableToOne" + ], + "type": "object", + "properties": { + "nonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInResponse" + } + ] + }, + "requiredNonNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInResponse" + } + ] + }, + "nullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/nullableToOneEmptyResourceInResponse" + } + ] + }, + "requiredNullableToOne": { + "allOf": [ + { + "$ref": "#/components/schemas/toOneEmptyResourceInResponse" + } + ] + }, + "toMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInResponse" + } + ] + }, + "requiredToMany": { + "allOf": [ + { + "$ref": "#/components/schemas/toManyEmptyResourceInResponse" + } + ] + } + }, + "additionalProperties": false + }, + "resourceResourceType": { + "enum": [ + "Resource" + ], + "type": "string" + }, + "toManyEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + } + }, + "additionalProperties": false + }, + "toManyEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "toOneEmptyResourceInRequest": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + } + }, + "additionalProperties": false + }, + "toOneEmptyResourceInResponse": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "allOf": [ + { + "$ref": "#/components/schemas/emptyResourceIdentifier" + } + ] + }, + "links": { + "allOf": [ + { + "$ref": "#/components/schemas/linksInRelationshipObject" + } + ] + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + } + } + } +} diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index 4b6c140fc4..f2c0237d1d 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -51,7 +51,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-post-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-post-request-document" + } + ] } } } @@ -149,7 +153,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-patch-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-patch-request-document" + } + ] } } } @@ -326,7 +334,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -356,7 +368,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -386,7 +402,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -444,7 +464,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-post-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-post-request-document" + } + ] } } } @@ -542,7 +566,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-patch-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-patch-request-document" + } + ] } } } @@ -719,7 +747,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -749,7 +781,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -779,7 +815,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -925,7 +965,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -955,7 +999,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -985,7 +1033,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } } } @@ -1043,7 +1095,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-post-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/flight-post-request-document" + } + ] } } } @@ -1141,7 +1197,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-patch-request-document" + "allOf": [ + { + "$ref": "#/components/schemas/flight-patch-request-document" + } + ] } } } @@ -1318,7 +1378,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + } + ] } } } @@ -1464,7 +1528,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] } } } @@ -1494,7 +1562,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] } } } @@ -1524,7 +1596,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] } } } @@ -1670,7 +1746,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } } } @@ -1700,7 +1780,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } } } @@ -1730,7 +1814,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } } } @@ -1876,7 +1964,11 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + } + ] } } } @@ -1970,6 +2062,9 @@ "additionalProperties": false }, "airplane-attributes-in-response": { + "required": [ + "name" + ], "type": "object", "properties": { "name": { @@ -2004,7 +2099,11 @@ "nullable": true }, "kind": { - "$ref": "#/components/schemas/aircraft-kind" + "allOf": [ + { + "$ref": "#/components/schemas/aircraft-kind" + } + ] } }, "additionalProperties": false @@ -2027,10 +2126,18 @@ "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -2043,17 +2150,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/airplane-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/airplane-attributes-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-attributes-in-patch-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/airplane-relationships-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-relationships-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2065,13 +2184,25 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/airplane-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-resource-type" + } + ] }, "attributes": { - "$ref": "#/components/schemas/airplane-attributes-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-attributes-in-post-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/airplane-relationships-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-relationships-in-post-request" + } + ] } }, "additionalProperties": false @@ -2085,20 +2216,36 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/airplane-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/airplane-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-attributes-in-response" + } + ] }, "relationships": { - "$ref": "#/components/schemas/airplane-relationships-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-relationships-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", @@ -2114,7 +2261,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/airplane-data-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-data-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2126,7 +2277,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/airplane-data-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-data-in-post-request" + } + ] } }, "additionalProperties": false @@ -2139,17 +2294,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/airplane-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/airplane-data-in-response" + } + ] }, "meta": { "type": "object", "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -2158,7 +2325,11 @@ "type": "object", "properties": { "flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } }, "additionalProperties": false @@ -2167,7 +2338,11 @@ "type": "object", "properties": { "flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } }, "additionalProperties": false @@ -2176,7 +2351,11 @@ "type": "object", "properties": { "flights": { - "$ref": "#/components/schemas/to-many-flight-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-response" + } + ] } }, "additionalProperties": false @@ -2238,6 +2417,10 @@ "additionalProperties": false }, "flight-attendant-attributes-in-response": { + "required": [ + "email-address", + "profile-image-url" + ], "type": "object", "properties": { "email-address": { @@ -2280,10 +2463,18 @@ "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -2296,17 +2487,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-attendant-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/flight-attendant-attributes-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-attributes-in-patch-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-attendant-relationships-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-relationships-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2318,13 +2521,25 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-attendant-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-resource-type" + } + ] }, "attributes": { - "$ref": "#/components/schemas/flight-attendant-attributes-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-attributes-in-post-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-attendant-relationships-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-relationships-in-post-request" + } + ] } }, "additionalProperties": false @@ -2338,20 +2553,36 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-attendant-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/flight-attendant-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-attributes-in-response" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-attendant-relationships-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-relationships-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", @@ -2368,7 +2599,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-attendant-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-resource-type" + } + ] }, "id": { "minLength": 1, @@ -2395,10 +2630,18 @@ "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + } + ] } }, "additionalProperties": false @@ -2411,17 +2654,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + ] }, "meta": { "type": "object", "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + } + ] } }, "additionalProperties": false @@ -2433,7 +2688,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2445,7 +2704,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-post-request" + } + ] } }, "additionalProperties": false @@ -2458,17 +2721,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + } + ] }, "meta": { "type": "object", "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -2477,10 +2752,18 @@ "type": "object", "properties": { "scheduled-for-flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] }, "purser-on-flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } }, "additionalProperties": false @@ -2489,10 +2772,18 @@ "type": "object", "properties": { "scheduled-for-flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] }, "purser-on-flights": { - "$ref": "#/components/schemas/to-many-flight-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-request" + } + ] } }, "additionalProperties": false @@ -2501,10 +2792,18 @@ "type": "object", "properties": { "scheduled-for-flights": { - "$ref": "#/components/schemas/to-many-flight-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-response" + } + ] }, "purser-on-flights": { - "$ref": "#/components/schemas/to-many-flight-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-in-response" + } + ] } }, "additionalProperties": false @@ -2523,17 +2822,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + } + ] }, "meta": { "type": "object", "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -2551,12 +2862,20 @@ "nullable": true }, "operated-by": { - "$ref": "#/components/schemas/airline" + "allOf": [ + { + "$ref": "#/components/schemas/airline" + } + ] } }, "additionalProperties": false }, "flight-attributes-in-response": { + "required": [ + "final-destination", + "services-on-board" + ], "type": "object", "properties": { "final-destination": { @@ -2569,7 +2888,11 @@ "nullable": true }, "operated-by": { - "$ref": "#/components/schemas/airline" + "allOf": [ + { + "$ref": "#/components/schemas/airline" + } + ] }, "departs-at": { "type": "string", @@ -2608,10 +2931,18 @@ "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -2624,17 +2955,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/flight-attributes-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attributes-in-patch-request" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-relationships-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-relationships-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2646,10 +2989,18 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-resource-type" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-relationships-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-relationships-in-post-request" + } + ] } }, "additionalProperties": false @@ -2663,20 +3014,36 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/flight-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attributes-in-response" + } + ] }, "relationships": { - "$ref": "#/components/schemas/flight-relationships-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-relationships-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", @@ -2693,7 +3060,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/flight-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/flight-resource-type" + } + ] }, "id": { "minLength": 1, @@ -2720,10 +3091,18 @@ "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + } + ] } }, "additionalProperties": false @@ -2735,7 +3114,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-data-in-patch-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-data-in-patch-request" + } + ] } }, "additionalProperties": false @@ -2747,7 +3130,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-data-in-post-request" + "allOf": [ + { + "$ref": "#/components/schemas/flight-data-in-post-request" + } + ] } }, "additionalProperties": false @@ -2760,17 +3147,29 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-data-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/flight-data-in-response" + } + ] }, "meta": { "type": "object", "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -2779,16 +3178,32 @@ "type": "object", "properties": { "cabin-crew-members": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] }, "purser": { - "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + } + ] }, "backup-purser": { - "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + } + ] }, "passengers": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } }, "additionalProperties": false @@ -2800,34 +3215,69 @@ "type": "object", "properties": { "cabin-crew-members": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-request" + } + ] }, "purser": { - "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-flight-attendant-in-request" + } + ] }, "backup-purser": { - "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-request" + } + ] }, "passengers": { - "$ref": "#/components/schemas/to-many-passenger-in-request" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-request" + } + ] } }, "additionalProperties": false }, "flight-relationships-in-response": { + "required": [ + "purser" + ], "type": "object", "properties": { "cabin-crew-members": { - "$ref": "#/components/schemas/to-many-flight-attendant-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-flight-attendant-in-response" + } + ] }, "purser": { - "$ref": "#/components/schemas/to-one-flight-attendant-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-one-flight-attendant-in-response" + } + ] }, "backup-purser": { - "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-in-response" + } + ] }, "passengers": { - "$ref": "#/components/schemas/to-many-passenger-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/to-many-passenger-in-response" + } + ] } }, "additionalProperties": false @@ -2996,29 +3446,6 @@ }, "additionalProperties": false }, - "null-value": { - "not": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "object" - }, - { - "type": "array" - } - ], - "items": {} - }, - "nullable": true - }, "nullable-flight-attendant-identifier-response-document": { "required": [ "data", @@ -3027,24 +3454,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "meta": { "type": "object", "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + } + ] } }, "additionalProperties": false @@ -3057,24 +3490,30 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/flight-attendant-data-in-response" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "meta": { "type": "object", "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-document" + } + ] } }, "additionalProperties": false @@ -3086,14 +3525,12 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true } }, "additionalProperties": false @@ -3105,17 +3542,19 @@ "type": "object", "properties": { "data": { - "oneOf": [ + "allOf": [ { "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" } - ] + ], + "nullable": true }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", @@ -3132,7 +3571,11 @@ "nullable": true }, "cabin-area": { - "$ref": "#/components/schemas/cabin-area" + "allOf": [ + { + "$ref": "#/components/schemas/cabin-area" + } + ] } }, "additionalProperties": false @@ -3155,10 +3598,18 @@ "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-collection-document" + } + ] } }, "additionalProperties": false @@ -3172,17 +3623,29 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/passenger-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/passenger-resource-type" + } + ] }, "id": { "minLength": 1, "type": "string" }, "attributes": { - "$ref": "#/components/schemas/passenger-attributes-in-response" + "allOf": [ + { + "$ref": "#/components/schemas/passenger-attributes-in-response" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-object" + } + ] }, "meta": { "type": "object", @@ -3199,7 +3662,11 @@ "type": "object", "properties": { "type": { - "$ref": "#/components/schemas/passenger-resource-type" + "allOf": [ + { + "$ref": "#/components/schemas/passenger-resource-type" + } + ] }, "id": { "minLength": 1, @@ -3226,10 +3693,18 @@ "additionalProperties": {} }, "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "allOf": [ + { + "$ref": "#/components/schemas/jsonapi-object" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + } + ] } }, "additionalProperties": false @@ -3268,7 +3743,11 @@ } }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", @@ -3305,7 +3784,11 @@ } }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", @@ -3342,7 +3825,11 @@ } }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", @@ -3358,7 +3845,11 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + ] } }, "additionalProperties": false @@ -3370,10 +3861,18 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/flight-attendant-identifier" + "allOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + ] }, "links": { - "$ref": "#/components/schemas/links-in-relationship-object" + "allOf": [ + { + "$ref": "#/components/schemas/links-in-relationship-object" + } + ] }, "meta": { "type": "object", diff --git a/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs b/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs index e0bbd5bad3..33372b10d9 100644 --- a/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/CamelCase/CamelCaseTests.cs @@ -43,9 +43,9 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("jsonapiObject"); + propertiesElement.ShouldContainPath("jsonapi.allOf[0].$ref").ShouldBeSchemaReferenceId("jsonapiObject"); - linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("linksInResourceCollectionDocument").SchemaReferenceId; resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.items.$ref").ShouldBeSchemaReferenceId("supermarketDataInResponse") @@ -69,16 +69,16 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("linksInResourceObject") + linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref").ShouldBeSchemaReferenceId("linksInResourceObject") .SchemaReferenceId; - primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("supermarketResourceType") + primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarketResourceType") .SchemaReferenceId; - resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.$ref") + resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarketAttributesInResponse").SchemaReferenceId; - resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarketRelationshipsInResponse").SchemaReferenceId; }); @@ -96,7 +96,7 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() { propertiesElement.Should().ContainProperty("nameOfCity"); propertiesElement.Should().ContainProperty("kind"); - propertiesElement.ShouldContainPath("kind.$ref").ShouldBeSchemaReferenceId("supermarketType"); + propertiesElement.ShouldContainPath("kind.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarketType"); }); string? nullableToOneResourceResponseDataSchemaRefId = null; @@ -105,13 +105,13 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() { propertiesElement.Should().ContainProperty("storeManager"); - propertiesElement.ShouldContainPath("storeManager.$ref").ShouldBeSchemaReferenceId("toOneStaffMemberInResponse"); + propertiesElement.ShouldContainPath("storeManager.allOf[0].$ref").ShouldBeSchemaReferenceId("toOneStaffMemberInResponse"); - nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("backupStoreManager.$ref") + nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("backupStoreManager.allOf[0].$ref") .ShouldBeSchemaReferenceId("nullableToOneStaffMemberInResponse").SchemaReferenceId; propertiesElement.Should().ContainProperty("cashiers"); - propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("toManyStaffMemberInResponse"); + propertiesElement.ShouldContainPath("cashiers.allOf[0].$ref").ShouldBeSchemaReferenceId("toManyStaffMemberInResponse"); }); string? linksInRelationshipObjectSchemaRefId = null; @@ -119,13 +119,11 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => { - linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("linksInRelationshipObject") - .SchemaReferenceId; + linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") + .ShouldBeSchemaReferenceId("linksInRelationshipObject").SchemaReferenceId; - relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.oneOf[0].$ref") + relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref") .ShouldBeSchemaReferenceId("staffMemberIdentifier").SchemaReferenceId; - - propertiesElement.ShouldContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("nullValue"); }); schemasElement.ShouldContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => @@ -138,7 +136,7 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => { - relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("staffMemberResourceType") + relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.allOf[0].$ref").ShouldBeSchemaReferenceId("staffMemberResourceType") .SchemaReferenceId; }); @@ -172,8 +170,8 @@ public async Task Casing_convention_is_applied_to_GetSingle_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("linksInResourceDocument") - .SchemaReferenceId; + linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") + .ShouldBeSchemaReferenceId("linksInResourceDocument").SchemaReferenceId; }); schemasElement.ShouldContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => @@ -210,13 +208,13 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_sin schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("staffMemberDataInResponse") + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref").ShouldBeSchemaReferenceId("staffMemberDataInResponse") .SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("staffMemberAttributesInResponse"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("staffMemberAttributesInResponse"); }); }); } @@ -285,7 +283,7 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("linksInResourceIdentifierDocument").SchemaReferenceId; }); @@ -343,7 +341,7 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("linksInResourceIdentifierCollectionDocument").SchemaReferenceId; }); @@ -376,7 +374,7 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() operationElement.ShouldBeString("postSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarketPostRequestDocument").SchemaReferenceId; }); @@ -386,7 +384,7 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarketDataInPostRequest") + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarketDataInPostRequest") .SchemaReferenceId; }); @@ -394,22 +392,22 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarketAttributesInPostRequest"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarketAttributesInPostRequest"); - resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarketRelationshipsInPostRequest").SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("storeManager"); - propertiesElement.ShouldContainPath("storeManager.$ref").ShouldBeSchemaReferenceId("toOneStaffMemberInRequest"); + propertiesElement.ShouldContainPath("storeManager.allOf[0].$ref").ShouldBeSchemaReferenceId("toOneStaffMemberInRequest"); propertiesElement.Should().ContainProperty("backupStoreManager"); - propertiesElement.ShouldContainPath("backupStoreManager.$ref").ShouldBeSchemaReferenceId("nullableToOneStaffMemberInRequest"); + propertiesElement.ShouldContainPath("backupStoreManager.allOf[0].$ref").ShouldBeSchemaReferenceId("nullableToOneStaffMemberInRequest"); propertiesElement.Should().ContainProperty("cashiers"); - propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("toManyStaffMemberInRequest"); + propertiesElement.ShouldContainPath("cashiers.allOf[0].$ref").ShouldBeSchemaReferenceId("toManyStaffMemberInRequest"); }); }); } @@ -446,7 +444,7 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() operationElement.ShouldBeString("patchSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarketPatchRequestDocument").SchemaReferenceId; }); @@ -456,14 +454,14 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarketDataInPatchRequest") + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarketDataInPatchRequest") .SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarketAttributesInPatchRequest"); - propertiesElement.ShouldContainPath("relationships.$ref").ShouldBeSchemaReferenceId("supermarketRelationshipsInPatchRequest"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarketAttributesInPatchRequest"); + propertiesElement.ShouldContainPath("relationships.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarketRelationshipsInPatchRequest"); }); }); } diff --git a/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs b/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs index 0a532a6e3f..47af397845 100644 --- a/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/KebabCase/KebabCaseTests.cs @@ -43,9 +43,9 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("jsonapi-object"); + propertiesElement.ShouldContainPath("jsonapi.allOf[0].$ref").ShouldBeSchemaReferenceId("jsonapi-object"); - linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("links-in-resource-collection-document").SchemaReferenceId; resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.items.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-response") @@ -69,16 +69,16 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("links-in-resource-object") - .SchemaReferenceId; + linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") + .ShouldBeSchemaReferenceId("links-in-resource-object").SchemaReferenceId; - primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("supermarket-resource-type") - .SchemaReferenceId; + primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.allOf[0].$ref") + .ShouldBeSchemaReferenceId("supermarket-resource-type").SchemaReferenceId; - resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.$ref") + resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarket-attributes-in-response").SchemaReferenceId; - resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarket-relationships-in-response").SchemaReferenceId; }); @@ -96,7 +96,7 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() { propertiesElement.Should().ContainProperty("name-of-city"); propertiesElement.Should().ContainProperty("kind"); - propertiesElement.ShouldContainPath("kind.$ref").ShouldBeSchemaReferenceId("supermarket-type"); + propertiesElement.ShouldContainPath("kind.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarket-type"); }); string? nullableToOneResourceResponseDataSchemaRefId = null; @@ -105,13 +105,13 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() { propertiesElement.Should().ContainProperty("store-manager"); - propertiesElement.ShouldContainPath("store-manager.$ref").ShouldBeSchemaReferenceId("to-one-staff-member-in-response"); + propertiesElement.ShouldContainPath("store-manager.allOf[0].$ref").ShouldBeSchemaReferenceId("to-one-staff-member-in-response"); - nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("backup-store-manager.$ref") + nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("backup-store-manager.allOf[0].$ref") .ShouldBeSchemaReferenceId("nullable-to-one-staff-member-in-response").SchemaReferenceId; propertiesElement.Should().ContainProperty("cashiers"); - propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("to-many-staff-member-in-response"); + propertiesElement.ShouldContainPath("cashiers.allOf[0].$ref").ShouldBeSchemaReferenceId("to-many-staff-member-in-response"); }); string? linksInRelationshipObjectSchemaRefId = null; @@ -119,13 +119,11 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => { - linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("links-in-relationship-object").SchemaReferenceId; - relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.oneOf[0].$ref") + relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref") .ShouldBeSchemaReferenceId("staff-member-identifier").SchemaReferenceId; - - propertiesElement.ShouldContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("null-value"); }); schemasElement.ShouldContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => @@ -138,8 +136,8 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => { - relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("staff-member-resource-type") - .SchemaReferenceId; + relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.allOf[0].$ref") + .ShouldBeSchemaReferenceId("staff-member-resource-type").SchemaReferenceId; }); schemasElement.ShouldContainPath($"{relatedResourceTypeSchemaRefId}.enum[0]").ShouldBeSchemaReferenceId("staff-members"); @@ -172,8 +170,8 @@ public async Task Casing_convention_is_applied_to_GetSingle_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("links-in-resource-document") - .SchemaReferenceId; + linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") + .ShouldBeSchemaReferenceId("links-in-resource-document").SchemaReferenceId; }); schemasElement.ShouldContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => @@ -210,13 +208,13 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_sin schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("staff-member-data-in-response") + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref").ShouldBeSchemaReferenceId("staff-member-data-in-response") .SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("staff-member-attributes-in-response"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("staff-member-attributes-in-response"); }); }); } @@ -285,7 +283,7 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("links-in-resource-identifier-document").SchemaReferenceId; }); @@ -343,7 +341,7 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("links-in-resource-identifier-collection-document").SchemaReferenceId; }); @@ -376,7 +374,7 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() operationElement.ShouldBeString("post-supermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarket-post-request-document").SchemaReferenceId; }); @@ -386,30 +384,30 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-post-request") - .SchemaReferenceId; + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref") + .ShouldBeSchemaReferenceId("supermarket-data-in-post-request").SchemaReferenceId; }); string? resourceRelationshipInPostRequestSchemaRefId = null; schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarket-attributes-in-post-request"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarket-attributes-in-post-request"); - resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarket-relationships-in-post-request").SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("store-manager"); - propertiesElement.ShouldContainPath("store-manager.$ref").ShouldBeSchemaReferenceId("to-one-staff-member-in-request"); + propertiesElement.ShouldContainPath("store-manager.allOf[0].$ref").ShouldBeSchemaReferenceId("to-one-staff-member-in-request"); propertiesElement.Should().ContainProperty("backup-store-manager"); - propertiesElement.ShouldContainPath("backup-store-manager.$ref").ShouldBeSchemaReferenceId("nullable-to-one-staff-member-in-request"); + propertiesElement.ShouldContainPath("backup-store-manager.allOf[0].$ref").ShouldBeSchemaReferenceId("nullable-to-one-staff-member-in-request"); propertiesElement.Should().ContainProperty("cashiers"); - propertiesElement.ShouldContainPath("cashiers.$ref").ShouldBeSchemaReferenceId("to-many-staff-member-in-request"); + propertiesElement.ShouldContainPath("cashiers.allOf[0].$ref").ShouldBeSchemaReferenceId("to-many-staff-member-in-request"); }); }); } @@ -446,7 +444,7 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() operationElement.ShouldBeString("patch-supermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.allOf[0].$ref") .ShouldBeSchemaReferenceId("supermarket-patch-request-document").SchemaReferenceId; }); @@ -456,14 +454,14 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("supermarket-data-in-patch-request") - .SchemaReferenceId; + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref") + .ShouldBeSchemaReferenceId("supermarket-data-in-patch-request").SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("supermarket-attributes-in-patch-request"); - propertiesElement.ShouldContainPath("relationships.$ref").ShouldBeSchemaReferenceId("supermarket-relationships-in-patch-request"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarket-attributes-in-patch-request"); + propertiesElement.ShouldContainPath("relationships.allOf[0].$ref").ShouldBeSchemaReferenceId("supermarket-relationships-in-patch-request"); }); }); } diff --git a/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs b/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs index 73868dcdad..c8cee4411b 100644 --- a/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs +++ b/test/OpenApiTests/NamingConventions/PascalCase/PascalCaseTests.cs @@ -44,9 +44,9 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("jsonapi.$ref").ShouldBeSchemaReferenceId("JsonapiObject"); + propertiesElement.ShouldContainPath("jsonapi.allOf[0].$ref").ShouldBeSchemaReferenceId("JsonapiObject"); - linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("LinksInResourceCollectionDocument").SchemaReferenceId; resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.items.$ref").ShouldBeSchemaReferenceId("SupermarketDataInResponse") @@ -70,16 +70,16 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("LinksInResourceObject") + linksInResourceObjectSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref").ShouldBeSchemaReferenceId("LinksInResourceObject") .SchemaReferenceId; - primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("SupermarketResourceType") + primaryResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.allOf[0].$ref").ShouldBeSchemaReferenceId("SupermarketResourceType") .SchemaReferenceId; - resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.$ref") + resourceAttributesInResponseSchemaRefId = propertiesElement.ShouldContainPath("attributes.allOf[0].$ref") .ShouldBeSchemaReferenceId("SupermarketAttributesInResponse").SchemaReferenceId; - resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInResponseSchemaRefId = propertiesElement.ShouldContainPath("relationships.allOf[0].$ref") .ShouldBeSchemaReferenceId("SupermarketRelationshipsInResponse").SchemaReferenceId; }); @@ -97,7 +97,7 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() { propertiesElement.Should().ContainProperty("NameOfCity"); propertiesElement.Should().ContainProperty("Kind"); - propertiesElement.ShouldContainPath("Kind.$ref").ShouldBeSchemaReferenceId("SupermarketType"); + propertiesElement.ShouldContainPath("Kind.allOf[0].$ref").ShouldBeSchemaReferenceId("SupermarketType"); }); string? nullableToOneResourceResponseDataSchemaRefId = null; @@ -106,13 +106,13 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() { propertiesElement.Should().ContainProperty("StoreManager"); - propertiesElement.ShouldContainPath("StoreManager.$ref").ShouldBeSchemaReferenceId("ToOneStaffMemberInResponse"); + propertiesElement.ShouldContainPath("StoreManager.allOf[0].$ref").ShouldBeSchemaReferenceId("ToOneStaffMemberInResponse"); - nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("BackupStoreManager.$ref") + nullableToOneResourceResponseDataSchemaRefId = propertiesElement.ShouldContainPath("BackupStoreManager.allOf[0].$ref") .ShouldBeSchemaReferenceId("NullableToOneStaffMemberInResponse").SchemaReferenceId; propertiesElement.Should().ContainProperty("Cashiers"); - propertiesElement.ShouldContainPath("Cashiers.$ref").ShouldBeSchemaReferenceId("ToManyStaffMemberInResponse"); + propertiesElement.ShouldContainPath("Cashiers.allOf[0].$ref").ShouldBeSchemaReferenceId("ToManyStaffMemberInResponse"); }); string? linksInRelationshipObjectSchemaRefId = null; @@ -120,13 +120,11 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{nullableToOneResourceResponseDataSchemaRefId}.properties").With(propertiesElement => { - linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("LinksInRelationshipObject") - .SchemaReferenceId; + linksInRelationshipObjectSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") + .ShouldBeSchemaReferenceId("LinksInRelationshipObject").SchemaReferenceId; - relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.oneOf[0].$ref") + relatedResourceIdentifierSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref") .ShouldBeSchemaReferenceId("StaffMemberIdentifier").SchemaReferenceId; - - propertiesElement.ShouldContainPath("data.oneOf[1].$ref").ShouldBeSchemaReferenceId("NullValue"); }); schemasElement.ShouldContainPath($"{linksInRelationshipObjectSchemaRefId}.properties").With(propertiesElement => @@ -139,7 +137,7 @@ public async Task Casing_convention_is_applied_to_GetCollection_endpoint() schemasElement.ShouldContainPath($"{relatedResourceIdentifierSchemaRefId}.properties").With(propertiesElement => { - relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.$ref").ShouldBeSchemaReferenceId("StaffMemberResourceType") + relatedResourceTypeSchemaRefId = propertiesElement.ShouldContainPath("type.allOf[0].$ref").ShouldBeSchemaReferenceId("StaffMemberResourceType") .SchemaReferenceId; }); @@ -173,8 +171,8 @@ public async Task Casing_convention_is_applied_to_GetSingle_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref").ShouldBeSchemaReferenceId("LinksInResourceDocument") - .SchemaReferenceId; + linksInResourceDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") + .ShouldBeSchemaReferenceId("LinksInResourceDocument").SchemaReferenceId; }); schemasElement.ShouldContainPath($"{linksInResourceDocumentSchemaRefId}.properties").With(propertiesElement => @@ -211,13 +209,13 @@ public async Task Casing_convention_is_applied_to_GetSecondary_endpoint_with_sin schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("StaffMemberDataInResponse") + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref").ShouldBeSchemaReferenceId("StaffMemberDataInResponse") .SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("StaffMemberAttributesInResponse"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("StaffMemberAttributesInResponse"); }); }); } @@ -286,7 +284,7 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("LinksInResourceIdentifierDocument").SchemaReferenceId; }); @@ -344,7 +342,7 @@ public async Task Casing_convention_is_applied_to_GetRelationship_endpoint_with_ schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.$ref") + linksInResourceIdentifierCollectionDocumentSchemaRefId = propertiesElement.ShouldContainPath("links.allOf[0].$ref") .ShouldBeSchemaReferenceId("LinksInResourceIdentifierCollectionDocument").SchemaReferenceId; }); @@ -377,7 +375,7 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() operationElement.ShouldBeString("PostSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.allOf[0].$ref") .ShouldBeSchemaReferenceId("SupermarketPostRequestDocument").SchemaReferenceId; }); @@ -387,7 +385,7 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("SupermarketDataInPostRequest") + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref").ShouldBeSchemaReferenceId("SupermarketDataInPostRequest") .SchemaReferenceId; }); @@ -395,22 +393,22 @@ public async Task Casing_convention_is_applied_to_Post_endpoint() schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("SupermarketAttributesInPostRequest"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("SupermarketAttributesInPostRequest"); - resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.$ref") + resourceRelationshipInPostRequestSchemaRefId = propertiesElement.ShouldContainPath("relationships.allOf[0].$ref") .ShouldBeSchemaReferenceId("SupermarketRelationshipsInPostRequest").SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceRelationshipInPostRequestSchemaRefId}.properties").With(propertiesElement => { propertiesElement.Should().ContainProperty("StoreManager"); - propertiesElement.ShouldContainPath("StoreManager.$ref").ShouldBeSchemaReferenceId("ToOneStaffMemberInRequest"); + propertiesElement.ShouldContainPath("StoreManager.allOf[0].$ref").ShouldBeSchemaReferenceId("ToOneStaffMemberInRequest"); propertiesElement.Should().ContainProperty("BackupStoreManager"); - propertiesElement.ShouldContainPath("BackupStoreManager.$ref").ShouldBeSchemaReferenceId("NullableToOneStaffMemberInRequest"); + propertiesElement.ShouldContainPath("BackupStoreManager.allOf[0].$ref").ShouldBeSchemaReferenceId("NullableToOneStaffMemberInRequest"); propertiesElement.Should().ContainProperty("Cashiers"); - propertiesElement.ShouldContainPath("Cashiers.$ref").ShouldBeSchemaReferenceId("ToManyStaffMemberInRequest"); + propertiesElement.ShouldContainPath("Cashiers.allOf[0].$ref").ShouldBeSchemaReferenceId("ToManyStaffMemberInRequest"); }); }); } @@ -447,7 +445,7 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() operationElement.ShouldBeString("PatchSupermarket"); }); - documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.$ref") + documentSchemaRefId = getElement.ShouldContainPath("requestBody.content['application/vnd.api+json'].schema.allOf[0].$ref") .ShouldBeSchemaReferenceId("SupermarketPatchRequestDocument").SchemaReferenceId; }); @@ -457,14 +455,14 @@ public async Task Casing_convention_is_applied_to_Patch_endpoint() schemasElement.ShouldContainPath($"{documentSchemaRefId}.properties").With(propertiesElement => { - resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.$ref").ShouldBeSchemaReferenceId("SupermarketDataInPatchRequest") + resourceDataSchemaRefId = propertiesElement.ShouldContainPath("data.allOf[0].$ref").ShouldBeSchemaReferenceId("SupermarketDataInPatchRequest") .SchemaReferenceId; }); schemasElement.ShouldContainPath($"{resourceDataSchemaRefId}.properties").With(propertiesElement => { - propertiesElement.ShouldContainPath("attributes.$ref").ShouldBeSchemaReferenceId("SupermarketAttributesInPatchRequest"); - propertiesElement.ShouldContainPath("relationships.$ref").ShouldBeSchemaReferenceId("SupermarketRelationshipsInPatchRequest"); + propertiesElement.ShouldContainPath("attributes.allOf[0].$ref").ShouldBeSchemaReferenceId("SupermarketAttributesInPatchRequest"); + propertiesElement.ShouldContainPath("relationships.allOf[0].$ref").ShouldBeSchemaReferenceId("SupermarketRelationshipsInPatchRequest"); }); }); } diff --git a/test/OpenApiTests/OpenApiStartup.cs b/test/OpenApiTests/OpenApiStartup.cs index 46978e7f67..e34ed1894c 100644 --- a/test/OpenApiTests/OpenApiStartup.cs +++ b/test/OpenApiTests/OpenApiStartup.cs @@ -6,7 +6,7 @@ namespace OpenApiTests; -public abstract class OpenApiStartup : TestableStartup +public class OpenApiStartup : TestableStartup where TDbContext : TestableDbContext { public override void ConfigureServices(IServiceCollection services) diff --git a/test/OpenApiTests/OpenApiTestContext.cs b/test/OpenApiTests/OpenApiTestContext.cs index f720df7d26..e54d09a269 100644 --- a/test/OpenApiTests/OpenApiTestContext.cs +++ b/test/OpenApiTests/OpenApiTestContext.cs @@ -2,7 +2,6 @@ using System.Text.Json; using JetBrains.Annotations; using TestBuildingBlocks; -using SysNotNull = System.Diagnostics.CodeAnalysis.NotNullAttribute; namespace OpenApiTests; @@ -27,34 +26,27 @@ internal async Task GetSwaggerDocumentAsync() private async Task CreateSwaggerDocumentAsync() { - string absoluteOutputPath = GetSwaggerDocumentAbsoluteOutputPath(SwaggerDocumentOutputPath); - string content = await GetAsync("swagger/v1/swagger.json"); JsonElement rootElement = ParseSwaggerDocument(content); - await WriteToDiskAsync(absoluteOutputPath, rootElement); + + if (SwaggerDocumentOutputPath != null) + { + string absoluteOutputPath = GetSwaggerDocumentAbsoluteOutputPath(SwaggerDocumentOutputPath); + await WriteToDiskAsync(absoluteOutputPath, rootElement); + } return rootElement; } - private static string GetSwaggerDocumentAbsoluteOutputPath(string? relativePath) + private static string GetSwaggerDocumentAbsoluteOutputPath(string relativePath) { - AssertHasSwaggerDocumentOutputPath(relativePath); - string solutionRoot = Path.Combine(Assembly.GetExecutingAssembly().Location, "../../../../../../"); string outputPath = Path.Combine(solutionRoot, relativePath, "swagger.g.json"); return Path.GetFullPath(outputPath); } - private static void AssertHasSwaggerDocumentOutputPath([SysNotNull] string? relativePath) - { - if (relativePath is null) - { - throw new Exception($"Property '{nameof(OpenApiTestContext)}.{nameof(SwaggerDocumentOutputPath)}' must be set."); - } - } - private async Task GetAsync(string requestUrl) { using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl); diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index d6cb1d3002..bbd9209a7b 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -6,7 +6,7 @@ - + @@ -17,6 +17,5 @@ - diff --git a/test/OpenApiTests/ResourceFieldValidation/EmptyResource.cs b/test/OpenApiTests/ResourceFieldValidation/EmptyResource.cs new file mode 100644 index 0000000000..0d7ddf5f1c --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/EmptyResource.cs @@ -0,0 +1,9 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; + +namespace OpenApiTests.ResourceFieldValidation; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public class EmptyResource : Identifiable +{ +} diff --git a/test/OpenApiTests/ResourceFieldValidation/ModelStateValidationDisabledStartup.cs b/test/OpenApiTests/ResourceFieldValidation/ModelStateValidationDisabledStartup.cs new file mode 100644 index 0000000000..0bea8b4d58 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/ModelStateValidationDisabledStartup.cs @@ -0,0 +1,17 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.Configuration; +using TestBuildingBlocks; + +namespace OpenApiTests.ResourceFieldValidation; + +[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] +public sealed class ModelStateValidationDisabledStartup : OpenApiStartup + where TDbContext : TestableDbContext +{ + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.ValidateModelState = false; + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/NullabilityTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/NullabilityTests.cs new file mode 100644 index 0000000000..d0d51fb350 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/NullabilityTests.cs @@ -0,0 +1,102 @@ +using System.Text.Json; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled; + +public sealed class NullabilityTests + : IClassFixture, NullableReferenceTypesDisabledDbContext>> +{ + private readonly OpenApiTestContext, NullableReferenceTypesDisabledDbContext> + _testContext; + + public NullabilityTests( + OpenApiTestContext, NullableReferenceTypesDisabledDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled"; + } + + [Theory] + [InlineData("referenceType")] + [InlineData("requiredReferenceType")] + [InlineData("nullableValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.ShouldContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + } + + [Theory] + [InlineData("valueType")] + [InlineData("requiredValueType")] + public async Task Schema_property_for_attribute_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.ShouldNotContainPath("nullable"); + }); + }); + } + + [Theory] + [InlineData("toOne")] + [InlineData("requiredToOne")] + public async Task Schema_property_for_relationship_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{jsonPropertyName}.allOf[0].$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").With(relationshipDataSchema => + { + relationshipDataSchema.ShouldContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + }); + } + + [Theory] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{jsonPropertyName}.allOf[0].$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").With(relationshipDataSchema => + { + relationshipDataSchema.ShouldNotContainPath("nullable"); + }); + }); + }); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/RequiredTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/RequiredTests.cs new file mode 100644 index 0000000000..5688462fb9 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationDisabled/RequiredTests.cs @@ -0,0 +1,106 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationDisabled; + +public sealed class RequiredTests + : IClassFixture, NullableReferenceTypesDisabledDbContext>> +{ + private readonly OpenApiTestContext, NullableReferenceTypesDisabledDbContext> + _testContext; + + public RequiredTests( + OpenApiTestContext, NullableReferenceTypesDisabledDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Theory] + [InlineData("requiredReferenceType")] + [InlineData("requiredValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("referenceType")] + [InlineData("valueType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithoutElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("requiredToOne")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("toOne")] + [InlineData("toMany")] + public async Task Schema_property_for_relationship_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithoutElement(jsonPropertyName)); + }); + } + + [Fact] + public async Task No_attribute_schema_properties_are_required_when_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.resourceAttributesInPatchRequest.required"); + } + + [Fact] + public async Task No_relationship_schema_properties_are_required_when_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.resourceRelationshipsInPatchRequest.required"); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/NullabilityTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/NullabilityTests.cs new file mode 100644 index 0000000000..534a7ed8b9 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/NullabilityTests.cs @@ -0,0 +1,100 @@ +using System.Text.Json; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled; + +public sealed class NullabilityTests + : IClassFixture, NullableReferenceTypesDisabledDbContext>> +{ + private readonly OpenApiTestContext, NullableReferenceTypesDisabledDbContext> _testContext; + + public NullabilityTests(OpenApiTestContext, NullableReferenceTypesDisabledDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled"; + } + + [Theory] + [InlineData("referenceType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.ShouldContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + } + + [Theory] + [InlineData("requiredReferenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.ShouldNotContainPath("nullable"); + }); + }); + } + + [Theory] + [InlineData("toOne")] + public async Task Schema_property_for_relationship_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{jsonPropertyName}.allOf[0].$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").With(relationshipDataSchema => + { + relationshipDataSchema.ShouldContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + }); + } + + [Theory] + [InlineData("requiredToOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{jsonPropertyName}.allOf[0].$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").With(relationshipDataSchema => + { + relationshipDataSchema.ShouldNotContainPath("nullable"); + }); + }); + }); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/RequiredTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/RequiredTests.cs new file mode 100644 index 0000000000..cba6e2035b --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/ModelStateValidationEnabled/RequiredTests.cs @@ -0,0 +1,104 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesDisabled.ModelStateValidationEnabled; + +public sealed class RequiredTests + : IClassFixture, NullableReferenceTypesDisabledDbContext>> +{ + private readonly OpenApiTestContext, NullableReferenceTypesDisabledDbContext> _testContext; + + public RequiredTests(OpenApiTestContext, NullableReferenceTypesDisabledDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Theory] + [InlineData("requiredReferenceType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("referenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithoutElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("requiredToOne")] + public async Task Schema_property_for_relationship_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("toOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithoutElement(jsonPropertyName)); + }); + } + + [Fact] + public async Task No_attribute_schema_properties_are_required_when_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.resourceAttributesInPatchRequest.required"); + } + + [Fact] + public async Task No_relationship_schema_properties_are_required_when_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.resourceRelationshipsInPatchRequest.required"); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/NrtDisabledResource.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/NrtDisabledResource.cs new file mode 100644 index 0000000000..4a297cf3e9 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/NrtDisabledResource.cs @@ -0,0 +1,48 @@ +#nullable disable + +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesDisabled; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(PublicName = "Resource", ControllerNamespace = "OpenApiTests.ResourceFieldValidation")] +public sealed class NrtDisabledResource : Identifiable +{ + [Attr] + public string ReferenceType { get; set; } + + [Attr] + [Required] + public string RequiredReferenceType { get; set; } + + [Attr] + public int ValueType { get; set; } + + [Attr] + [Required] + public int RequiredValueType { get; set; } + + [Attr] + public int? NullableValueType { get; set; } + + [Attr] + [Required] + public int? RequiredNullableValueType { get; set; } + + [HasOne] + public EmptyResource ToOne { get; set; } + + [Required] + [HasOne] + public EmptyResource RequiredToOne { get; set; } + + [HasMany] + public ICollection ToMany { get; set; } = new HashSet(); + + [Required] + [HasMany] + public ICollection RequiredToMany { get; set; } = new HashSet(); +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/NullableReferenceTypesDisabledDbContext.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/NullableReferenceTypesDisabledDbContext.cs new file mode 100644 index 0000000000..1e8691d9de --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesDisabled/NullableReferenceTypesDisabledDbContext.cs @@ -0,0 +1,35 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesDisabled; + +// @formatter:wrap_chained_method_calls chop_always + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class NullableReferenceTypesDisabledDbContext : TestableDbContext +{ + public DbSet NrtDisabledResources => Set(); + + public NullableReferenceTypesDisabledDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(resource => resource.ToOne); + + builder.Entity() + .HasOne(resource => resource.RequiredToOne); + + builder.Entity() + .HasMany(resource => resource.ToMany); + + builder.Entity() + .HasMany(resource => resource.RequiredToMany); + + base.OnModelCreating(builder); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/NullabilityTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/NullabilityTests.cs new file mode 100644 index 0000000000..23e58e54bd --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/NullabilityTests.cs @@ -0,0 +1,106 @@ +using System.Text.Json; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled; + +public sealed class NullabilityTests + : IClassFixture, NullableReferenceTypesEnabledDbContext>> +{ + private readonly OpenApiTestContext, NullableReferenceTypesEnabledDbContext> + _testContext; + + public NullabilityTests( + OpenApiTestContext, NullableReferenceTypesEnabledDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled"; + } + + [Theory] + [InlineData("nullableReferenceType")] + [InlineData("requiredNullableReferenceType")] + [InlineData("nullableValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.ShouldContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + } + + [Theory] + [InlineData("nonNullableReferenceType")] + [InlineData("requiredNonNullableReferenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + public async Task Schema_property_for_attribute_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.ShouldNotContainPath("nullable"); + }); + }); + } + + [Theory] + [InlineData("nullableToOne")] + [InlineData("requiredNullableToOne")] + public async Task Schema_property_for_relationship_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{jsonPropertyName}.allOf[0].$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").With(relationshipDataSchema => + { + relationshipDataSchema.ShouldContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + }); + } + + [Theory] + [InlineData("nonNullableToOne")] + [InlineData("requiredNonNullableToOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{jsonPropertyName}.allOf[0].$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").With(relationshipDataSchema => + { + relationshipDataSchema.ShouldNotContainPath("nullable"); + }); + }); + }); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/RequiredTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/RequiredTests.cs new file mode 100644 index 0000000000..0cca901d44 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationDisabled/RequiredTests.cs @@ -0,0 +1,110 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationDisabled; + +public sealed class RequiredTests + : IClassFixture, NullableReferenceTypesEnabledDbContext>> +{ + private readonly OpenApiTestContext, NullableReferenceTypesEnabledDbContext> + _testContext; + + public RequiredTests( + OpenApiTestContext, NullableReferenceTypesEnabledDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Theory] + [InlineData("requiredNonNullableReferenceType")] + [InlineData("requiredNullableReferenceType")] + [InlineData("requiredValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nonNullableReferenceType")] + [InlineData("nullableReferenceType")] + [InlineData("valueType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithoutElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("requiredNonNullableToOne")] + [InlineData("requiredNullableToOne")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nonNullableToOne")] + [InlineData("nullableToOne")] + [InlineData("toMany")] + public async Task Schema_property_for_relationship_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithoutElement(jsonPropertyName)); + }); + } + + [Fact] + public async Task No_attribute_schema_properties_are_required_when_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.resourceAttributesInPatchRequest.required"); + } + + [Fact] + public async Task No_relationship_schema_properties_are_required_when_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.resourceRelationshipsInPatchRequest.required"); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/NullabilityTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/NullabilityTests.cs new file mode 100644 index 0000000000..a2fb76c38f --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/NullabilityTests.cs @@ -0,0 +1,104 @@ +using System.Text.Json; +using FluentAssertions; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled; + +public sealed class NullabilityTests + : IClassFixture, NullableReferenceTypesEnabledDbContext>> +{ + private readonly OpenApiTestContext, NullableReferenceTypesEnabledDbContext> _testContext; + + public NullabilityTests(OpenApiTestContext, NullableReferenceTypesEnabledDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + testContext.SwaggerDocumentOutputPath = "test/OpenApiClientTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled"; + } + + [Theory] + [InlineData("nullableReferenceType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.ShouldContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + } + + [Theory] + [InlineData("nonNullableReferenceType")] + [InlineData("requiredNonNullableReferenceType")] + [InlineData("requiredNullableReferenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInResponse.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath(jsonPropertyName).With(schemaProperty => + { + schemaProperty.ShouldNotContainPath("nullable"); + }); + }); + } + + [Theory] + [InlineData("nullableToOne")] + public async Task Schema_property_for_relationship_is_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{jsonPropertyName}.allOf[0].$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").With(relationshipDataSchema => + { + relationshipDataSchema.ShouldContainPath("nullable").With(nullableProperty => nullableProperty.ValueKind.Should().Be(JsonValueKind.True)); + }); + }); + }); + } + + [Theory] + [InlineData("nonNullableToOne")] + [InlineData("requiredNonNullableToOne")] + [InlineData("requiredNullableToOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_nullable(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest.properties").With(schemaProperties => + { + schemaProperties.ShouldContainPath($"{jsonPropertyName}.allOf[0].$ref").WithSchemaReferenceId(schemaReferenceId => + { + document.ShouldContainPath($"components.schemas.{schemaReferenceId}.properties.data").With(relationshipDataSchema => + { + relationshipDataSchema.ShouldNotContainPath("nullable"); + }); + }); + }); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/RequiredTests.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/RequiredTests.cs new file mode 100644 index 0000000000..f0fbc13272 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/ModelStateValidationEnabled/RequiredTests.cs @@ -0,0 +1,108 @@ +using System.Text.Json; +using TestBuildingBlocks; +using Xunit; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesEnabled.ModelStateValidationEnabled; + +public sealed class RequiredTests + : IClassFixture, NullableReferenceTypesEnabledDbContext>> +{ + private readonly OpenApiTestContext, NullableReferenceTypesEnabledDbContext> _testContext; + + public RequiredTests(OpenApiTestContext, NullableReferenceTypesEnabledDbContext> testContext) + { + _testContext = testContext; + + testContext.UseController(); + } + + [Theory] + [InlineData("nonNullableReferenceType")] + [InlineData("requiredNonNullableReferenceType")] + [InlineData("requiredNullableReferenceType")] + [InlineData("requiredNullableValueType")] + public async Task Schema_property_for_attribute_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nullableReferenceType")] + [InlineData("valueType")] + [InlineData("requiredValueType")] + [InlineData("nullableValueType")] + public async Task Schema_property_for_attribute_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceAttributesInPostRequest").With(attributesObjectSchema => + { + attributesObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + attributesObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithoutElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nonNullableToOne")] + [InlineData("requiredNonNullableToOne")] + [InlineData("requiredNullableToOne")] + public async Task Schema_property_for_relationship_is_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithElement(jsonPropertyName)); + }); + } + + [Theory] + [InlineData("nullableToOne")] + [InlineData("toMany")] + [InlineData("requiredToMany")] + public async Task Schema_property_for_relationship_is_not_required_for_creating_resource(string jsonPropertyName) + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldContainPath("components.schemas.resourceRelationshipsInPostRequest").With(relationshipsObjectSchema => + { + relationshipsObjectSchema.ShouldContainPath($"properties.{jsonPropertyName}"); + relationshipsObjectSchema.ShouldContainPath("required").With(propertySet => propertySet.ShouldBeArrayWithoutElement(jsonPropertyName)); + }); + } + + [Fact] + public async Task No_attribute_schema_properties_are_required_when_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.resourceAttributesInPatchRequest.required"); + } + + [Fact] + public async Task No_relationship_schema_properties_are_required_when_updating_resource() + { + // Act + JsonElement document = await _testContext.GetSwaggerDocumentAsync(); + + // Assert + document.ShouldNotContainPath("components.schemas.resourceRelationshipsInPatchRequest.required"); + } +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/NrtEnabledResource.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/NrtEnabledResource.cs new file mode 100644 index 0000000000..8908f2a50f --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/NrtEnabledResource.cs @@ -0,0 +1,60 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesEnabled; + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +[Resource(PublicName = "Resource", ControllerNamespace = "OpenApiTests.ResourceFieldValidation")] +public sealed class NrtEnabledResource : Identifiable +{ + [Attr] + public string NonNullableReferenceType { get; set; } = null!; + + [Attr] + [Required] + public string RequiredNonNullableReferenceType { get; set; } = null!; + + [Attr] + public string? NullableReferenceType { get; set; } + + [Attr] + [Required] + public string? RequiredNullableReferenceType { get; set; } + + [Attr] + public int ValueType { get; set; } + + [Attr] + [Required] + public int RequiredValueType { get; set; } + + [Attr] + public int? NullableValueType { get; set; } + + [Attr] + [Required] + public int? RequiredNullableValueType { get; set; } + + [HasOne] + public EmptyResource NonNullableToOne { get; set; } = null!; + + [Required] + [HasOne] + public EmptyResource RequiredNonNullableToOne { get; set; } = null!; + + [HasOne] + public EmptyResource? NullableToOne { get; set; } + + [Required] + [HasOne] + public EmptyResource? RequiredNullableToOne { get; set; } + + [HasMany] + public ICollection ToMany { get; set; } = new HashSet(); + + [Required] + [HasMany] + public ICollection RequiredToMany { get; set; } = new HashSet(); +} diff --git a/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/NullableReferenceTypesEnabledDbContext.cs b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/NullableReferenceTypesEnabledDbContext.cs new file mode 100644 index 0000000000..54f1620db5 --- /dev/null +++ b/test/OpenApiTests/ResourceFieldValidation/NullableReferenceTypesEnabled/NullableReferenceTypesEnabledDbContext.cs @@ -0,0 +1,41 @@ +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using TestBuildingBlocks; + +namespace OpenApiTests.ResourceFieldValidation.NullableReferenceTypesEnabled; + +// @formatter:wrap_chained_method_calls chop_always + +[UsedImplicitly(ImplicitUseTargetFlags.Members)] +public sealed class NullableReferenceTypesEnabledDbContext : TestableDbContext +{ + public DbSet NullableReferenceTypesEnabledResources => Set(); + + public NullableReferenceTypesEnabledDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasOne(resource => resource.NonNullableToOne); + + builder.Entity() + .HasOne(resource => resource.RequiredNonNullableToOne); + + builder.Entity() + .HasOne(resource => resource.NullableToOne); + + builder.Entity() + .HasOne(resource => resource.RequiredNullableToOne); + + builder.Entity() + .HasMany(resource => resource.ToMany); + + builder.Entity() + .HasMany(resource => resource.RequiredToMany); + + base.OnModelCreating(builder); + } +} diff --git a/test/OpenApiTests/JsonElementExtensions.cs b/test/TestBuildingBlocks/JsonElementExtensions.cs similarity index 56% rename from test/OpenApiTests/JsonElementExtensions.cs rename to test/TestBuildingBlocks/JsonElementExtensions.cs index d8dc7d4e20..6eafecc4ca 100644 --- a/test/OpenApiTests/JsonElementExtensions.cs +++ b/test/TestBuildingBlocks/JsonElementExtensions.cs @@ -2,11 +2,11 @@ using BlushingPenguin.JsonPath; using FluentAssertions; using FluentAssertions.Execution; -using TestBuildingBlocks; +using JetBrains.Annotations; -namespace OpenApiTests; +namespace TestBuildingBlocks; -internal static class JsonElementExtensions +public static class JsonElementExtensions { public static JsonElementAssertions Should(this JsonElement source) { @@ -19,13 +19,50 @@ public static JsonElement ShouldContainPath(this JsonElement source, string path return elementSelector.Should().NotThrow().Subject; } + public static void ShouldNotContainPath(this JsonElement source, string path) + { + JsonElement? pathToken = source.SelectToken(path); + + pathToken.Should().BeNull(); + } + public static void ShouldBeString(this JsonElement source, string value) { source.ValueKind.Should().Be(JsonValueKind.String); source.GetString().Should().Be(value); } + public static void ShouldBeArrayWithElement(this JsonElement source, string value) + { + source.ValueKind.Should().Be(JsonValueKind.Array); + + var deserializedCollection = JsonSerializer.Deserialize>(source.GetRawText()); + deserializedCollection.Should().Contain(value); + } + + public static void ShouldBeArrayWithoutElement(this JsonElement source, string value) + { + source.ValueKind.Should().Be(JsonValueKind.Array); + + var deserializedCollection = JsonSerializer.Deserialize>(source.GetRawText()); + deserializedCollection.Should().NotContain(value); + } + + public static void ShouldBeInteger(this JsonElement source, int value) + { + source.ValueKind.Should().Be(JsonValueKind.Number); + source.GetInt32().Should().Be(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); @@ -33,16 +70,21 @@ 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 { public string SchemaReferenceId { get; } - public SchemaReferenceIdContainer(string schemaReferenceId) + internal SchemaReferenceIdContainer(string schemaReferenceId) { SchemaReferenceId = schemaReferenceId; } @@ -50,7 +92,7 @@ public SchemaReferenceIdContainer(string schemaReferenceId) public sealed class JsonElementAssertions : JsonElementAssertions { - public JsonElementAssertions(JsonElement subject) + internal JsonElementAssertions(JsonElement subject) : base(subject) { } diff --git a/test/TestBuildingBlocks/TestBuildingBlocks.csproj b/test/TestBuildingBlocks/TestBuildingBlocks.csproj index ce8c54ef3b..55e2830bb9 100644 --- a/test/TestBuildingBlocks/TestBuildingBlocks.csproj +++ b/test/TestBuildingBlocks/TestBuildingBlocks.csproj @@ -8,6 +8,7 @@ +