From c6e85425f1022a12a3662cc364dc68bf2c91ee0e Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 18 Nov 2021 13:45:35 +0100 Subject: [PATCH] Process review feedback --- ...onApiActionDescriptorCollectionProvider.cs | 16 +- .../JsonApiEndpointMetadataProvider.cs | 80 +++--- .../PrimaryResourceResponseDocument.cs | 3 + .../NullableToOneRelationshipRequestData.cs | 12 + .../NullableToOneRelationshipResponseData.cs | 19 ++ .../JsonApiObjects/SingleData.cs | 2 +- .../JsonApiOperationIdSelector.cs | 13 +- .../JsonApiRequestFormatMetadataProvider.cs | 1 + .../JsonApiSchemaIdSelector.cs | 2 + .../MemberInfoExtensions.cs | 87 +------ .../OpenApiEndpointConvention.cs | 12 +- .../ResourceFieldAttributeExtensions.cs | 22 ++ .../ServiceCollectionExtensions.cs | 6 +- .../JsonApiSchemaGenerator.cs | 1 + .../ResourceFieldObjectSchemaBuilder.cs | 124 ++++++---- .../{DataTypeClass.cs => TypeCategory.cs} | 4 +- .../JsonApiDotNetCore.csproj | 2 +- .../LegacyOpenApiIntegration/Flight.cs | 5 +- .../FlightAttendant.cs | 2 + .../LegacyIntegrationDbContext.cs | 4 + .../LegacyOpenApiIntegration/swagger.json | 230 ++++++++++++++++-- 21 files changed, 434 insertions(+), 213 deletions(-) create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/NullableToOneRelationshipRequestData.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/NullableToOneRelationshipResponseData.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs rename src/JsonApiDotNetCore.OpenApi/{DataTypeClass.cs => TypeCategory.cs} (65%) diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs index 9a988351b5..232d978938 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.OpenApi.JsonApiMetadata; using Microsoft.AspNetCore.Mvc; @@ -26,15 +25,14 @@ internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescrip public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors(); - public JsonApiActionDescriptorCollectionProvider(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping, + public JsonApiActionDescriptorCollectionProvider(IControllerResourceMapping controllerResourceMapping, IActionDescriptorCollectionProvider defaultProvider) { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); ArgumentGuard.NotNull(defaultProvider, nameof(defaultProvider)); _defaultProvider = defaultProvider; - _jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(resourceGraph, controllerResourceMapping); + _jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(controllerResourceMapping); } private ActionDescriptorCollection GetActionDescriptors() @@ -107,7 +105,6 @@ private static void UpdateProducesResponseTypeAttribute(ActionDescriptor endpoin if (producesResponse != null) { producesResponse.Type = responseTypeToSet; - return; } } @@ -134,7 +131,7 @@ private static IList Expand(ActionDescriptor genericEndpoint, if (expandedEndpoint.AttributeRouteInfo == null) { - throw new NotSupportedException("Only attribute based routing is supported for JsonApiDotNetCore endpoints"); + throw new UnreachableCodeException(); } ExpandTemplate(expandedEndpoint.AttributeRouteInfo, relationshipName); @@ -172,7 +169,12 @@ private static ActionDescriptor Clone(ActionDescriptor descriptor) { var clonedDescriptor = (ActionDescriptor)descriptor.MemberwiseClone(); - clonedDescriptor.AttributeRouteInfo = (AttributeRouteInfo)descriptor.AttributeRouteInfo!.MemberwiseClone(); + if (descriptor.AttributeRouteInfo == null) + { + throw new NotSupportedException("Only attribute routing is supported for JsonApiDotNetCore endpoints."); + } + + clonedDescriptor.AttributeRouteInfo = (AttributeRouteInfo)descriptor.AttributeRouteInfo.MemberwiseClone(); clonedDescriptor.FilterDescriptors = new List(); diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs index b3b939c709..6e694105b7 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs @@ -16,16 +16,13 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata /// internal sealed class JsonApiEndpointMetadataProvider { - private readonly IResourceGraph _resourceGraph; private readonly IControllerResourceMapping _controllerResourceMapping; private readonly EndpointResolver _endpointResolver = new(); - public JsonApiEndpointMetadataProvider(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping) + public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping) { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); - _resourceGraph = resourceGraph; _controllerResourceMapping = controllerResourceMapping; } @@ -47,28 +44,28 @@ public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction) throw new UnreachableCodeException(); } - IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(endpoint.Value, primaryResourceType.ClrType); - IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(endpoint.Value, primaryResourceType.ClrType); + IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(endpoint.Value, primaryResourceType); + IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(endpoint.Value, primaryResourceType); return new JsonApiEndpointMetadataContainer(requestMetadata, responseMetadata); } - private IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoint endpoint, Type primaryResourceType) + private static IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoint endpoint, ResourceType primaryResourceType) { switch (endpoint) { case JsonApiEndpoint.Post: { - return GetPostRequestMetadata(primaryResourceType); + return GetPostRequestMetadata(primaryResourceType.ClrType); } case JsonApiEndpoint.Patch: { - return GetPatchRequestMetadata(primaryResourceType); + return GetPatchRequestMetadata(primaryResourceType.ClrType); } case JsonApiEndpoint.PostRelationship: case JsonApiEndpoint.PatchRelationship: case JsonApiEndpoint.DeleteRelationship: { - return GetRelationshipRequestMetadata(primaryResourceType, endpoint != JsonApiEndpoint.PatchRelationship); + return GetRelationshipRequestMetadata(primaryResourceType.Relationships, endpoint != JsonApiEndpoint.PatchRelationship); } default: { @@ -77,38 +74,45 @@ public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction) } } - private static PrimaryRequestMetadata GetPostRequestMetadata(Type primaryResourceType) + private static PrimaryRequestMetadata GetPostRequestMetadata(Type resourceClrType) { - Type documentType = typeof(ResourcePostRequestDocument<>).MakeGenericType(primaryResourceType); + Type documentType = typeof(ResourcePostRequestDocument<>).MakeGenericType(resourceClrType); return new PrimaryRequestMetadata(documentType); } - private static PrimaryRequestMetadata GetPatchRequestMetadata(Type primaryResourceType) + private static PrimaryRequestMetadata GetPatchRequestMetadata(Type resourceClrType) { - Type documentType = typeof(ResourcePatchRequestDocument<>).MakeGenericType(primaryResourceType); + Type documentType = typeof(ResourcePatchRequestDocument<>).MakeGenericType(resourceClrType); return new PrimaryRequestMetadata(documentType); } - private RelationshipRequestMetadata GetRelationshipRequestMetadata(Type primaryResourceType, bool ignoreHasOneRelationships) + private static RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable relationships, + bool ignoreHasOneRelationships) { - IEnumerable relationships = _resourceGraph.GetResourceType(primaryResourceType).Relationships; + IEnumerable relationshipInRequest = ignoreHasOneRelationships ? relationships.OfType() : relationships; - if (ignoreHasOneRelationships) - { - relationships = relationships.OfType(); - } + IDictionary resourceTypesByRelationshipName = relationshipInRequest.ToDictionary(relationship => relationship.PublicName, + relationship => + { + // @formatter:nested_ternary_style expanded - IDictionary resourceTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, - relationship => relationship is HasManyAttribute - ? typeof(ToManyRelationshipRequestData<>).MakeGenericType(relationship.RightType.ClrType) - : typeof(ToOneRelationshipRequestData<>).MakeGenericType(relationship.RightType.ClrType)); + Type requestDataType = relationship is HasManyAttribute + ? typeof(ToManyRelationshipRequestData<>) + : relationship.IsNullable() + ? typeof(NullableToOneRelationshipRequestData<>) + : typeof(ToOneRelationshipRequestData<>); + + // @formatter:nested_ternary_style restore + + return requestDataType.MakeGenericType(relationship.RightType.ClrType); + }); return new RelationshipRequestMetadata(resourceTypesByRelationshipName); } - private IJsonApiResponseMetadata? GetResponseMetadata(JsonApiEndpoint endpoint, Type primaryResourceType) + private static IJsonApiResponseMetadata? GetResponseMetadata(JsonApiEndpoint endpoint, ResourceType primaryResourceType) { switch (endpoint) { @@ -117,15 +121,15 @@ private RelationshipRequestMetadata GetRelationshipRequestMetadata(Type primaryR case JsonApiEndpoint.Post: case JsonApiEndpoint.Patch: { - return GetPrimaryResponseMetadata(primaryResourceType, endpoint == JsonApiEndpoint.GetCollection); + return GetPrimaryResponseMetadata(primaryResourceType.ClrType, endpoint == JsonApiEndpoint.GetCollection); } case JsonApiEndpoint.GetSecondary: { - return GetSecondaryResponseMetadata(primaryResourceType); + return GetSecondaryResponseMetadata(primaryResourceType.Relationships); } case JsonApiEndpoint.GetRelationship: { - return GetRelationshipResponseMetadata(primaryResourceType); + return GetRelationshipResponseMetadata(primaryResourceType.Relationships); } default: { @@ -134,17 +138,17 @@ private RelationshipRequestMetadata GetRelationshipRequestMetadata(Type primaryR } } - private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type primaryResourceType, bool endpointReturnsCollection) + private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type resourceClrType, bool endpointReturnsCollection) { Type documentOpenType = endpointReturnsCollection ? typeof(ResourceCollectionResponseDocument<>) : typeof(PrimaryResourceResponseDocument<>); - Type documentType = documentOpenType.MakeGenericType(primaryResourceType); + Type documentType = documentOpenType.MakeGenericType(resourceClrType); return new PrimaryResponseMetadata(documentType); } - private SecondaryResponseMetadata GetSecondaryResponseMetadata(Type primaryResourceType) + private static SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable relationships) { - IDictionary responseTypesByRelationshipName = GetMetadataByRelationshipName(primaryResourceType, relationship => + IDictionary responseTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, relationship => { Type documentType = relationship is HasManyAttribute ? typeof(ResourceCollectionResponseDocument<>) @@ -156,17 +160,9 @@ private SecondaryResponseMetadata GetSecondaryResponseMetadata(Type primaryResou return new SecondaryResponseMetadata(responseTypesByRelationshipName); } - private IDictionary GetMetadataByRelationshipName(Type primaryResourceType, - Func extractRelationshipMetadataCallback) - { - IReadOnlyCollection relationships = _resourceGraph.GetResourceType(primaryResourceType).Relationships; - - return relationships.ToDictionary(relationship => relationship.PublicName, extractRelationshipMetadataCallback); - } - - private RelationshipResponseMetadata GetRelationshipResponseMetadata(Type primaryResourceType) + private static RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable relationships) { - IDictionary responseTypesByRelationshipName = GetMetadataByRelationshipName(primaryResourceType, + IDictionary responseTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, relationship => relationship is HasManyAttribute ? typeof(ResourceIdentifierCollectionResponseDocument<>).MakeGenericType(relationship.RightType.ClrType) : typeof(ResourceIdentifierResponseDocument<>).MakeGenericType(relationship.RightType.ClrType)); diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs index 493a2cf445..bbfd508ab4 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs @@ -7,6 +7,9 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents { + /// Types in the namespace are never touched by ASP.NET ModelState validation, therefore using a + /// non-nullable reference type for a property does not imply this property is required. Instead, we rely on explicitly setting + /// which is how Swashbuckle is instructed to mark properties as required. [UsedImplicitly(ImplicitUseTargetFlags.Members)] internal sealed class PrimaryResourceResponseDocument : SingleData> where TResource : IIdentifiable diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/NullableToOneRelationshipRequestData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/NullableToOneRelationshipRequestData.cs new file mode 100644 index 0000000000..06a6354a10 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/NullableToOneRelationshipRequestData.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class NullableToOneRelationshipRequestData : SingleData?> + where TResource : IIdentifiable + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/NullableToOneRelationshipResponseData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/NullableToOneRelationshipResponseData.cs new file mode 100644 index 0000000000..3bd06e4c43 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/NullableToOneRelationshipResponseData.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class NullableToOneRelationshipResponseData : SingleData?> + where TResource : IIdentifiable + { + [Required] + public LinksInRelationshipObject Links { get; set; } = null!; + + public IDictionary Meta { get; set; } = null!; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs index 0e447e256a..a2ef118cfb 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs @@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects { [UsedImplicitly(ImplicitUseTargetFlags.Members)] internal abstract class SingleData - where TData : ResourceIdentifierObject + where TData : ResourceIdentifierObject? { [Required] public TData Data { get; set; } = null!; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs index 07ce2fe7de..b5b5b12d94 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs @@ -31,6 +31,7 @@ internal sealed class JsonApiOperationIdSelector [typeof(ResourceIdentifierCollectionResponseDocument<>)] = RelationshipOperationIdTemplate, [typeof(ResourceIdentifierResponseDocument<>)] = RelationshipOperationIdTemplate, [typeof(ToOneRelationshipRequestData<>)] = RelationshipOperationIdTemplate, + [typeof(NullableToOneRelationshipRequestData<>)] = RelationshipOperationIdTemplate, [typeof(ToManyRelationshipRequestData<>)] = RelationshipOperationIdTemplate }; @@ -64,14 +65,14 @@ public string GetOperationId(ApiDescription endpoint) return ApplyTemplate(template, primaryResourceType.ClrType, endpoint); } - private static string GetTemplate(Type primaryResourceType, ApiDescription endpoint) + private static string GetTemplate(Type resourceClrType, ApiDescription endpoint) { - Type requestDocumentType = GetDocumentType(primaryResourceType, endpoint); + Type requestDocumentType = GetDocumentType(resourceClrType, endpoint); return DocumentOpenTypeToOperationIdTemplateMap[requestDocumentType]; } - private static Type GetDocumentType(Type primaryResourceType, ApiDescription endpoint) + private static Type GetDocumentType(Type resourceClrType, ApiDescription endpoint) { var producesResponseTypeAttribute = endpoint.ActionDescriptor.GetFilterMetadata(); @@ -89,7 +90,7 @@ private static Type GetDocumentType(Type primaryResourceType, ApiDescription end { Type documentResourceType = producesResponseTypeAttribute.Type.GetGenericArguments()[0]; - if (documentResourceType != primaryResourceType) + if (documentResourceType != resourceClrType) { documentType = typeof(SecondaryResourceResponseDocument<>); } @@ -103,10 +104,10 @@ private static Type GetDocumentType(Type primaryResourceType, ApiDescription end return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null; } - private string ApplyTemplate(string operationIdTemplate, Type primaryResourceType, ApiDescription endpoint) + private string ApplyTemplate(string operationIdTemplate, Type resourceClrType, ApiDescription endpoint) { string method = endpoint.HttpMethod!.ToLowerInvariant(); - string primaryResourceName = _formatter.FormatResourceName(primaryResourceType).Singularize(); + string primaryResourceName = _formatter.FormatResourceName(resourceClrType).Singularize(); string relationshipName = operationIdTemplate.Contains("[RelationshipName]") ? endpoint.RelativePath.Split("/").Last() : string.Empty; // @formatter:wrap_chained_method_calls chop_always diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs index 1fef9854f0..e3d2a0a17a 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs @@ -17,6 +17,7 @@ internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IA { typeof(ToManyRelationshipRequestData<>), typeof(ToOneRelationshipRequestData<>), + typeof(NullableToOneRelationshipRequestData<>), typeof(ResourcePostRequestDocument<>), typeof(ResourcePatchRequestDocument<>) }; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs index 352eba0584..3d81d87e13 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -18,6 +18,7 @@ internal sealed class JsonApiSchemaIdSelector [typeof(ResourcePostRequestObject<>)] = "###-data-in-post-request", [typeof(ResourcePatchRequestObject<>)] = "###-data-in-patch-request", [typeof(ToOneRelationshipRequestData<>)] = "to-one-###-request-data", + [typeof(NullableToOneRelationshipRequestData<>)] = "nullable-to-one-###-request-data", [typeof(ToManyRelationshipRequestData<>)] = "to-many-###-request-data", [typeof(PrimaryResourceResponseDocument<>)] = "###-primary-response-document", [typeof(SecondaryResourceResponseDocument<>)] = "###-secondary-response-document", @@ -25,6 +26,7 @@ internal sealed class JsonApiSchemaIdSelector [typeof(ResourceIdentifierResponseDocument<>)] = "###-identifier-response-document", [typeof(ResourceIdentifierCollectionResponseDocument<>)] = "###-identifier-collection-response-document", [typeof(ToOneRelationshipResponseData<>)] = "to-one-###-response-data", + [typeof(NullableToOneRelationshipResponseData<>)] = "nullable-to-one-###-response-data", [typeof(ToManyRelationshipResponseData<>)] = "to-many-###-response-data", [typeof(ResourceResponseObject<>)] = "###-data-in-response", [typeof(ResourceIdentifierObject<>)] = "###-identifier" diff --git a/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs b/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs index 8d8c31c7ca..5f10b64aac 100644 --- a/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs @@ -1,97 +1,24 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; +using Swashbuckle.AspNetCore.SwaggerGen; namespace JsonApiDotNetCore.OpenApi { internal static class MemberInfoExtensions { - private const string NullableAttributeFullTypeName = "System.Runtime.CompilerServices.NullableAttribute"; - private const string NullableFlagsFieldName = "NullableFlags"; - private const string NullableContextAttributeFullTypeName = "System.Runtime.CompilerServices.NullableContextAttribute"; - private const string FlagFieldName = "Flag"; - - /// - /// Resolves the class of data type of the - /// - /// . - /// - public static DataTypeClass ResolveDataType(this MemberInfo memberInfo) + public static TypeCategory ResolveDataTypeCategory(this MemberInfo source) { - ArgumentGuard.NotNull(memberInfo, nameof(memberInfo)); - - // Based on https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/67344fe0b7c7e78128159d8bf02ebfe91408c3da/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs#L36 - - Type memberType = memberInfo.MemberType == MemberTypes.Field ? ((FieldInfo)memberInfo).FieldType : ((PropertyInfo)memberInfo).PropertyType; - - Type? underlyingType = Nullable.GetUnderlyingType(memberType); + ArgumentGuard.NotNull(source, nameof(source)); - if (underlyingType != null) - { - return underlyingType.IsValueType ? DataTypeClass.NullableValueType : DataTypeClass.NullableReferenceType; - } + Type memberType = source.MemberType == MemberTypes.Field ? ((FieldInfo)source).FieldType : ((PropertyInfo)source).PropertyType; if (memberType.IsValueType) { - return DataTypeClass.ValueType; + return Nullable.GetUnderlyingType(memberType) != null ? TypeCategory.NullableValueType : TypeCategory.ValueType; } - Attribute? nullableAttribute = GetNullableAttribute(memberInfo); - - if (nullableAttribute == null) - { - return HasNullableContextAttribute(memberInfo) ? DataTypeClass.NullableReferenceType : DataTypeClass.NonNullableReferenceType; - } - - return HasNullableFlag(nullableAttribute) ? DataTypeClass.NonNullableReferenceType : DataTypeClass.NullableReferenceType; - } - - private static Attribute? GetNullableAttribute(MemberInfo memberInfo) - { - Attribute? nullableAttribute = memberInfo.GetCustomAttributes() - .FirstOrDefault(attr => string.Equals(attr.GetType().FullName, NullableAttributeFullTypeName)); - - return nullableAttribute; - } - - private static bool HasNullableContextAttribute(MemberInfo memberInfo) - { - if (memberInfo.DeclaringType?.DeclaringType == null) - { - return false; - } - - Type[] declaringTypes = memberInfo.DeclaringType is { IsNested: true } - ? new[] - { - memberInfo.DeclaringType, - memberInfo.DeclaringType.DeclaringType - } - : new[] - { - memberInfo.DeclaringType - }; - - foreach (Type type in declaringTypes) - { - var attributes = (IEnumerable)type.GetCustomAttributes(false); - - object? nullableContext = attributes.FirstOrDefault(attr => string.Equals(attr.GetType().FullName, NullableContextAttributeFullTypeName)); - - if (nullableContext != null) - { - return nullableContext.GetType().GetField(FlagFieldName) is { } field && field.GetValue(nullableContext) is byte and 1; - } - } - - return false; - } - - private static bool HasNullableFlag(Attribute nullableAttribute) - { - FieldInfo? fieldInfo = nullableAttribute.GetType().GetField(NullableFlagsFieldName); - return fieldInfo is { } nullableFlagsField && nullableFlagsField.GetValue(nullableAttribute) is byte[] { Length: >= 1 } flags && flags[0] == 1; + // Once we switch to .NET 6 lands, this should be replaced with the built-in reflection support for nullability. + return source.IsNonNullableReferenceType() ? TypeCategory.ReferenceType : TypeCategory.NullableReferenceType; } } } diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs index 7bc0178df1..c4c5dfb7e9 100644 --- a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs @@ -16,16 +16,13 @@ namespace JsonApiDotNetCore.OpenApi /// internal sealed class OpenApiEndpointConvention : IActionModelConvention { - private readonly IResourceGraph _resourceGraph; private readonly IControllerResourceMapping _controllerResourceMapping; private readonly EndpointResolver _endpointResolver = new(); - public OpenApiEndpointConvention(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping) + public OpenApiEndpointConvention(IControllerResourceMapping controllerResourceMapping) { - ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); - _resourceGraph = resourceGraph; _controllerResourceMapping = controllerResourceMapping; } @@ -69,9 +66,12 @@ private bool ShouldSuppressEndpoint(JsonApiEndpoint endpoint, Type controllerTyp private IReadOnlyCollection GetRelationshipsOfPrimaryResource(Type controllerType) { - ResourceType primaryResourceTypeOfEndpoint = _controllerResourceMapping.GetResourceTypeForController(controllerType)!; + ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(controllerType); - ResourceType primaryResourceType = _resourceGraph.GetResourceType(primaryResourceTypeOfEndpoint.ClrType); + if (primaryResourceType == null) + { + throw new UnreachableCodeException(); + } return primaryResourceType.Relationships; } diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs new file mode 100644 index 0000000000..14d72d6f15 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs @@ -0,0 +1,22 @@ +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.ResolveDataTypeCategory(); + bool hasRequiredAttribute = source.Property.HasAttribute(); + + return fieldTypeCategory switch + { + TypeCategory.ReferenceType or TypeCategory.ValueType => false, + TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => !hasRequiredAttribute, + _ => throw new UnreachableCodeException() + }; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index 0cf26b312a..8f2a781fbd 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -38,13 +38,12 @@ private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBu { services.AddSingleton(provider => { - var resourceGraph = provider.GetRequiredService(); var controllerResourceMapping = provider.GetRequiredService(); var actionDescriptorCollectionProvider = provider.GetRequiredService(); var apiDescriptionProviders = provider.GetRequiredService>(); JsonApiActionDescriptorCollectionProvider descriptorCollectionProviderWrapper = - new(resourceGraph, controllerResourceMapping, actionDescriptorCollectionProvider); + new(controllerResourceMapping, actionDescriptorCollectionProvider); return new ApiDescriptionGroupCollectionProvider(descriptorCollectionProviderWrapper, apiDescriptionProviders); }); @@ -130,10 +129,9 @@ private static void AddSwashbuckleCliCompatibility(IServiceScope scope, IMvcCore private static void AddOpenApiEndpointConvention(IServiceScope scope, IMvcCoreBuilder mvcBuilder) { - var resourceGraph = scope.ServiceProvider.GetRequiredService(); var controllerResourceMapping = scope.ServiceProvider.GetRequiredService(); - mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(resourceGraph, controllerResourceMapping))); + mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(controllerResourceMapping))); } } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index 0a7c97f62f..eacefa9603 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -24,6 +24,7 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator private static readonly Type[] SingleNonPrimaryDataDocumentOpenTypes = { typeof(ToOneRelationshipRequestData<>), + typeof(NullableToOneRelationshipRequestData<>), typeof(ResourceIdentifierResponseDocument<>), typeof(SecondaryResourceResponseDocument<>) }; diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs index a0e673fb2c..6d2c4cae88 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Reflection; using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Resources.Annotations; @@ -15,6 +14,19 @@ internal sealed class ResourceFieldObjectSchemaBuilder { private static readonly SchemaRepository ResourceSchemaRepository = new(); + private static readonly Type[] RelationshipResponseDataOpenTypes = + { + typeof(ToOneRelationshipResponseData<>), + typeof(ToManyRelationshipResponseData<>), + typeof(NullableToOneRelationshipResponseData<>) + }; + + private static readonly Type[] NullableRelationshipDataOpenTypes = + { + typeof(NullableToOneRelationshipRequestData<>), + typeof(NullableToOneRelationshipResponseData<>) + }; + private readonly ResourceTypeInfo _resourceTypeInfo; private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; private readonly SchemaGenerator _defaultSchemaGenerator; @@ -83,17 +95,11 @@ private void SetMembersOfAttributesObject(OpenApiSchema fullSchemaForAttributesO { AddAttributeSchemaToResourceObject(matchingAttribute, fullSchemaForAttributesObject, resourceFieldSchema); - DataTypeClass propertyDataTypeClass = matchingAttribute.Property.ResolveDataType(); - bool hasRequiredAttribute = matchingAttribute.Property.GetCustomAttribute() != null; - - resourceFieldSchema.Nullable = IsAttributeNullable(propertyDataTypeClass, hasRequiredAttribute); + resourceFieldSchema.Nullable = matchingAttribute.IsNullable(); - if (_resourceTypeInfo.ResourceObjectOpenType == typeof(ResourcePostRequestObject<>)) + if (IsFieldRequired(matchingAttribute)) { - if (IsAttributeRequired(propertyDataTypeClass, hasRequiredAttribute)) - { - fullSchemaForAttributesObject.Required.Add(matchingAttribute.PublicName); - } + fullSchemaForAttributesObject.Required.Add(matchingAttribute.PublicName); } } } @@ -116,34 +122,32 @@ private void AddAttributeSchemaToResourceObject(AttrAttribute attribute, OpenApi attributesObjectSchema.Properties.Add(attribute.PublicName, resourceAttributeSchema); } - private static bool IsAttributeNullable(DataTypeClass dataTypeClass, bool hasRequiredAttribute) + private void ExposeSchema(OpenApiReference openApiReference, Type typeRepresentedBySchema) { - return dataTypeClass switch - { - DataTypeClass.NonNullableReferenceType or DataTypeClass.ValueType => false, - DataTypeClass.NullableReferenceType or DataTypeClass.NullableValueType => !hasRequiredAttribute, - _ => throw new UnreachableCodeException() - }; + OpenApiSchema fullSchema = ResourceSchemaRepository.Schemas[openApiReference.Id]; + _schemaRepositoryAccessor.Current.AddDefinition(openApiReference.Id, fullSchema); + _schemaRepositoryAccessor.Current.RegisterType(typeRepresentedBySchema, openApiReference.Id); } - private static bool IsAttributeRequired(DataTypeClass dataTypeClass, bool hasRequiredAttribute) + private bool IsFieldRequired(ResourceFieldAttribute field) { - return dataTypeClass switch + if (_resourceTypeInfo.ResourceObjectOpenType != typeof(ResourcePostRequestObject<>)) { - DataTypeClass.NonNullableReferenceType => true, - DataTypeClass.ValueType => hasRequiredAttribute, - DataTypeClass.NullableReferenceType or DataTypeClass.NullableValueType => hasRequiredAttribute, + return false; + } + + TypeCategory fieldTypeCategory = field.Property.ResolveDataTypeCategory(); + bool hasRequiredAttribute = field.Property.HasAttribute(); + + return fieldTypeCategory switch + { + TypeCategory.ReferenceType => true, + TypeCategory.ValueType => hasRequiredAttribute, + TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => hasRequiredAttribute, _ => throw new UnreachableCodeException() }; } - private void ExposeSchema(OpenApiReference openApiReference, Type typeRepresentedBySchema) - { - OpenApiSchema fullSchema = ResourceSchemaRepository.Schemas[openApiReference.Id]; - _schemaRepositoryAccessor.Current.AddDefinition(openApiReference.Id, fullSchema); - _schemaRepositoryAccessor.Current.RegisterType(typeRepresentedBySchema, openApiReference.Id); - } - private OpenApiSchema GetReferenceSchemaForFieldObject(OpenApiSchema fullSchema, string fieldObjectName) { // NSwag does not have proper support for using an inline schema for the attributes and relationships object in a resource object, see https://github.com/RicoSuter/NSwag/issues/3474. Once this issue has been resolved, we can remove this. @@ -212,28 +216,54 @@ private void GenerateResourceIdentifierObjectSchema(Type resourceIdentifierObjec fullSchemaForResourceIdentifierObject.Properties[JsonApiObjectPropertyName.Type] = _resourceTypeSchemaGenerator.Get(resourceType); } - private void AddRelationshipDataSchemaToResourceObject(RelationshipAttribute relationship, OpenApiSchema relationshipObjectSchema) + private void AddRelationshipDataSchemaToResourceObject(RelationshipAttribute relationship, OpenApiSchema fullSchemaForRelationshipObject) { Type relationshipDataType = GetRelationshipDataType(relationship, _resourceTypeInfo.ResourceObjectOpenType); - OpenApiSchema referenceSchemaForRelationshipData = GetReferenceSchemaForRelationshipData(relationshipDataType) ?? - CreateRelationshipDataObjectSchema(relationship, relationshipDataType); + OpenApiSchema relationshipDataSchema = GetReferenceSchemaForRelationshipData(relationshipDataType) ?? + CreateRelationshipDataObjectSchema(relationshipDataType); - relationshipObjectSchema.Properties.Add(relationship.PublicName, referenceSchemaForRelationshipData); + fullSchemaForRelationshipObject.Properties.Add(relationship.PublicName, relationshipDataSchema); + + if (IsFieldRequired(relationship)) + { + fullSchemaForRelationshipObject.Required.Add(relationship.PublicName); + } } private static Type GetRelationshipDataType(RelationshipAttribute relationship, Type resourceObjectType) { - if (resourceObjectType.GetGenericTypeDefinition().IsAssignableTo(typeof(ResourceResponseObject<>))) - { - return relationship is HasOneAttribute - ? typeof(ToOneRelationshipResponseData<>).MakeGenericType(relationship.RightType.ClrType) - : typeof(ToManyRelationshipResponseData<>).MakeGenericType(relationship.RightType.ClrType); - } + Type relationshipDataType = resourceObjectType.GetGenericTypeDefinition().IsAssignableTo(typeof(ResourceResponseObject<>)) + ? GetRelationshipDataTypeForResponse(relationship) + : GetRelationshipDataTypeForRequest(relationship); + + return relationshipDataType.MakeGenericType(relationship.RightType.ClrType); + } + + private static Type GetRelationshipDataTypeForRequest(RelationshipAttribute relationship) + { + // @formatter:nested_ternary_style expanded + + return relationship is HasOneAttribute + ? relationship.IsNullable() + ? typeof(NullableToOneRelationshipRequestData<>) + : typeof(ToOneRelationshipRequestData<>) + : typeof(ToManyRelationshipRequestData<>); + + // @formatter:nested_ternary_style restore + } + + private static Type GetRelationshipDataTypeForResponse(RelationshipAttribute relationship) + { + // @formatter:nested_ternary_style expanded return relationship is HasOneAttribute - ? typeof(ToOneRelationshipRequestData<>).MakeGenericType(relationship.RightType.ClrType) - : typeof(ToManyRelationshipRequestData<>).MakeGenericType(relationship.RightType.ClrType); + ? relationship.IsNullable() + ? typeof(NullableToOneRelationshipResponseData<>) + : typeof(ToOneRelationshipResponseData<>) + : typeof(ToManyRelationshipResponseData<>); + + // @formatter:nested_ternary_style restore } private OpenApiSchema? GetReferenceSchemaForRelationshipData(Type relationshipDataType) @@ -242,7 +272,7 @@ private static Type GetRelationshipDataType(RelationshipAttribute relationship, return referenceSchemaForRelationshipData; } - private OpenApiSchema CreateRelationshipDataObjectSchema(RelationshipAttribute relationship, Type relationshipDataType) + private OpenApiSchema CreateRelationshipDataObjectSchema(Type relationshipDataType) { OpenApiSchema referenceSchema = _defaultSchemaGenerator.GenerateSchema(relationshipDataType, _schemaRepositoryAccessor.Current); @@ -250,15 +280,15 @@ private OpenApiSchema CreateRelationshipDataObjectSchema(RelationshipAttribute r Type relationshipDataOpenType = relationshipDataType.GetGenericTypeDefinition(); - if (relationshipDataOpenType == typeof(ToOneRelationshipResponseData<>) || relationshipDataOpenType == typeof(ToManyRelationshipResponseData<>)) + if (NullableRelationshipDataOpenTypes.Contains(relationshipDataOpenType)) { - fullSchema.Required.Remove(JsonApiObjectPropertyName.Data); + fullSchema.Properties[JsonApiObjectPropertyName.Data] = + _nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiObjectPropertyName.Data]); } - if (relationship is HasOneAttribute) + if (RelationshipResponseDataOpenTypes.Contains(relationshipDataOpenType)) { - fullSchema.Properties[JsonApiObjectPropertyName.Data] = - _nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiObjectPropertyName.Data]); + fullSchema.Required.Remove(JsonApiObjectPropertyName.Data); } return referenceSchema; diff --git a/src/JsonApiDotNetCore.OpenApi/DataTypeClass.cs b/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs similarity index 65% rename from src/JsonApiDotNetCore.OpenApi/DataTypeClass.cs rename to src/JsonApiDotNetCore.OpenApi/TypeCategory.cs index bd114c4f45..14c7c23e5d 100644 --- a/src/JsonApiDotNetCore.OpenApi/DataTypeClass.cs +++ b/src/JsonApiDotNetCore.OpenApi/TypeCategory.cs @@ -1,9 +1,9 @@ namespace JsonApiDotNetCore.OpenApi { - internal enum DataTypeClass + internal enum TypeCategory { + ReferenceType, NullableReferenceType, - NonNullableReferenceType, ValueType, NullableValueType } diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 8809659e65..4867102d63 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -7,7 +7,7 @@ jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net - A framework for building JSON:API compliant REST APIs using .NET Core and Entity Framework Core. Includes support for Atomic Operations. The ultimate goal of this library is to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination. You just need to focus on defining the resources and implementing your custom business logic. This library has been designed around dependency injection making extensibility incredibly easy. + A framework for building JSON:API compliant REST APIs using ASP.NET and Entity Framework Core. Includes support for Atomic Operations. The ultimate goal of this library is to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination. You just need to focus on defining the resources and implementing your custom business logic. This library has been designed around dependency injection making extensibility incredibly easy. json-api-dotnet https://www.jsonapi.net/ MIT diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs index c5a26a34c5..d5608d45ce 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs @@ -34,9 +34,12 @@ public sealed class Flight : Identifiable [HasOne] public FlightAttendant Purser { get; set; } = null!; + [HasOne] + public FlightAttendant? BackupPurser { get; set; } + [Attr] [NotMapped] - public ICollection ServicesOnBoard { get; set; } = null!; + public ICollection ServicesOnBoard { get; set; } = new HashSet(); [HasMany] public ICollection Passengers { get; set; } = new HashSet(); diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs index 1cf2de622d..d4f09593e5 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs @@ -36,5 +36,7 @@ public sealed class FlightAttendant : Identifiable [HasMany] public ISet PurserOnFlights { get; set; } = new HashSet(); + + public ISet BackupPurserOnFlights { get; set; } = new HashSet(); } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs index 6b9d861327..04b0b822dc 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs @@ -27,6 +27,10 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .HasOne(flight => flight.Purser) .WithMany(flightAttendant => flightAttendant.PurserOnFlights); + + builder.Entity() + .HasOne(flight => flight.BackupPurser) + .WithMany(flightAttendant => flightAttendant!.BackupPurserOnFlights); } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index eff75497b6..c2d9411296 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -1184,6 +1184,152 @@ } } }, + "/api/v1/flights/{id}/backup-purser": { + "get": { + "tags": [ + "flights" + ], + "operationId": "get-flight-backup-purser", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-secondary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flights" + ], + "operationId": "head-flight-backup-purser", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-secondary-response-document" + } + } + } + } + } + } + }, + "/api/v1/flights/{id}/relationships/backup-purser": { + "get": { + "tags": [ + "flights" + ], + "operationId": "get-flight-backup-purser-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-identifier-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flights" + ], + "operationId": "head-flight-backup-purser-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-identifier-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "flights" + ], + "operationId": "patch-flight-backup-purser-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, "/api/v1/flights/{id}/cabin-crew-members": { "get": { "tags": [ @@ -2016,6 +2162,9 @@ "additionalProperties": false }, "airplane-relationships-in-post-request": { + "required": [ + "flights" + ], "type": "object", "properties": { "flights": { @@ -2339,6 +2488,10 @@ "additionalProperties": false }, "flight-attendant-relationships-in-post-request": { + "required": [ + "scheduled-for-flights", + "purser-on-flights" + ], "type": "object", "properties": { "scheduled-for-flights": { @@ -2641,6 +2794,9 @@ "purser": { "$ref": "#/components/schemas/to-one-flight-attendant-request-data" }, + "backup-purser": { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-request-data" + }, "passengers": { "$ref": "#/components/schemas/to-many-passenger-request-data" } @@ -2648,6 +2804,11 @@ "additionalProperties": false }, "flight-relationships-in-post-request": { + "required": [ + "cabin-crew-members", + "purser", + "passengers" + ], "type": "object", "properties": { "cabin-crew-members": { @@ -2656,6 +2817,9 @@ "purser": { "$ref": "#/components/schemas/to-one-flight-attendant-request-data" }, + "backup-purser": { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-request-data" + }, "passengers": { "$ref": "#/components/schemas/to-many-passenger-request-data" } @@ -2671,6 +2835,9 @@ "purser": { "$ref": "#/components/schemas/to-one-flight-attendant-response-data" }, + "backup-purser": { + "$ref": "#/components/schemas/nullable-to-one-flight-attendant-response-data" + }, "passengers": { "$ref": "#/components/schemas/to-many-passenger-response-data" } @@ -2853,6 +3020,51 @@ }, "nullable": true }, + "nullable-to-one-flight-attendant-request-data": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "nullable-to-one-flight-attendant-response-data": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + }, + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, "passenger-attributes-in-response": { "type": "object", "properties": { @@ -3085,14 +3297,7 @@ "type": "object", "properties": { "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] + "$ref": "#/components/schemas/flight-attendant-identifier" } }, "additionalProperties": false @@ -3104,14 +3309,7 @@ "type": "object", "properties": { "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] + "$ref": "#/components/schemas/flight-attendant-identifier" }, "links": { "$ref": "#/components/schemas/links-in-relationship-object"