From 1c099e736cfdc3bb1b94d49fb97f7b32f4ac99c0 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 8 Sep 2021 10:55:16 +0200 Subject: [PATCH 01/35] Copied integration and fixed compilation errors --- .../ActionDescriptorExtensions.cs | 39 +++ ...onApiActionDescriptorCollectionProvider.cs | 196 ++++++++++++++ .../JsonApiEndpoint.cs | 18 ++ .../JsonApiInputFormatterWithMetadata.cs | 41 +++ .../JsonApiMetadata/EndpointResolver.cs | 53 ++++ .../ExpansibleEndpointMetadata.cs | 10 + .../IJsonApiEndpointMetadata.cs | 6 + .../IJsonApiRequestMetadata.cs | 6 + .../IJsonApiResponseMetadata.cs | 6 + .../JsonApiEndpointMetadataContainer.cs | 12 + .../JsonApiEndpointMetadataProvider.cs | 187 ++++++++++++++ .../JsonApiMetadata/PrimaryRequestMetadata.cs | 9 + .../PrimaryResponseMetadata.cs | 9 + .../RelationshipRequestMetadata.cs | 12 + .../RelationshipResponseMetadata.cs | 12 + .../SecondaryResponseMetadata.cs | 12 + .../JsonApiObjectPropertyName.cs | 19 ++ .../PrimaryResourceResponseDocument.cs | 21 ++ .../ResourceCollectionResponseDocument.cs | 21 ++ ...rceIdentifierCollectionResponseDocument.cs | 21 ++ .../ResourceIdentifierResponseDocument.cs | 21 ++ .../Documents/ResourcePatchRequestDocument.cs | 12 + .../Documents/ResourcePostRequestDocument.cs | 12 + .../SecondaryResourceResponseDocument.cs | 21 ++ .../JsonApiObjects/JsonapiObject.cs | 17 ++ .../Links/LinksInRelationshipObject.cs | 15 ++ .../LinksInResourceCollectionDocument.cs | 23 ++ .../Links/LinksInResourceDocument.cs | 14 + ...sInResourceIdentifierCollectionDocument.cs | 26 ++ .../LinksInResourceIdentifierDocument.cs | 17 ++ .../Links/LinksInResourceObject.cs | 12 + .../JsonApiObjects/ManyData.cs | 15 ++ .../ToManyRelationshipRequestData.cs | 12 + .../ToManyRelationshipResponseData.cs | 19 ++ .../ToOneRelationshipRequestData.cs | 12 + .../ToOneRelationshipResponseData.cs | 19 ++ .../ResourceIdentifierObject.cs | 22 ++ .../ResourceObjects/ResourceObject.cs | 15 ++ .../ResourcePatchRequestObject.cs | 9 + .../ResourcePostRequestObject.cs | 9 + .../ResourceObjects/ResourceResponseObject.cs | 18 ++ .../JsonApiObjects/SingleData.cs | 14 + .../JsonApiOperationIdSelector.cs | 114 ++++++++ .../JsonApiPathParameter.cs | 9 + .../JsonApiRoutingTemplate.cs | 13 + .../JsonApiSchemaIdSelector.cs | 69 +++++ .../ObjectExtensions.cs | 19 ++ .../OpenApiEndpointConvention.cs | 155 ++++++++++- .../ParameterInfoExtensions.cs | 37 +++ .../ResourceNameFormatterProxy.cs | 50 ++++ .../ServiceCollectionExtensions.cs | 123 ++++++++- .../CachingSwaggerGenerator.cs | 32 +++ .../EndpointOrderingFilter.cs | 49 ++++ .../ISchemaRepositoryAccessor.cs | 9 + .../JsonApiDataContractResolver.cs | 115 +++++++++ .../JsonApiObjectNullabilityProcessor.cs | 132 ++++++++++ .../JsonApiSchemaGenerator.cs | 159 ++++++++++++ .../NullableReferenceSchemaGenerator.cs | 97 +++++++ .../NullableReferenceSchemaStrategy.cs | 8 + .../ResourceFieldObjectSchemaBuilder.cs | 243 ++++++++++++++++++ .../ResourceObjectSchemaGenerator.cs | 150 +++++++++++ .../SwaggerComponents/ResourceTypeInfo.cs | 45 ++++ .../ResourceTypeSchemaGenerator.cs | 59 +++++ .../SchemaRepositoryAccessor.cs | 29 +++ .../UnreachableCodeException.cs | 12 + src/JsonApiDotNetCore/TypeExtensions.cs | 29 +++ 66 files changed, 2817 insertions(+), 4 deletions(-) create mode 100644 src/JsonApiDotNetCore.OpenApi/ActionDescriptorExtensions.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiEndpoint.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiInputFormatterWithMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/ExpansibleEndpointMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiEndpointMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiRequestMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiResponseMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataContainer.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/PrimaryRequestMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/PrimaryResponseMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipRequestMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipResponseMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/SecondaryResponseMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjectPropertyName.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonapiObject.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationshipObject.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceCollectionDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceIdentifierCollectionDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceIdentifierDocument.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceObject.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToManyRelationshipRequestData.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToManyRelationshipResponseData.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToOneRelationshipRequestData.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToOneRelationshipResponseData.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifierObject.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObject.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourcePatchRequestObject.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourcePostRequestObject.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceResponseObject.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiPathParameter.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiRoutingTemplate.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/ObjectExtensions.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/ParameterInfoExtensions.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/ResourceNameFormatterProxy.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/EndpointOrderingFilter.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ISchemaRepositoryAccessor.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiObjectNullabilityProcessor.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaStrategy.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/SwaggerComponents/SchemaRepositoryAccessor.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/UnreachableCodeException.cs diff --git a/src/JsonApiDotNetCore.OpenApi/ActionDescriptorExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ActionDescriptorExtensions.cs new file mode 100644 index 0000000000..e0d4ae4f35 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/ActionDescriptorExtensions.cs @@ -0,0 +1,39 @@ +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace JsonApiDotNetCore.OpenApi +{ + internal static class ActionDescriptorExtensions + { + public static MethodInfo GetActionMethod(this ActionDescriptor descriptor) + { + ArgumentGuard.NotNull(descriptor, nameof(descriptor)); + + return ((ControllerActionDescriptor)descriptor).MethodInfo; + } + + public static TFilterMetaData GetFilterMetadata(this ActionDescriptor descriptor) + where TFilterMetaData : IFilterMetadata + { + ArgumentGuard.NotNull(descriptor, nameof(descriptor)); + + IFilterMetadata filterMetadata = descriptor.FilterDescriptors.Select(filterDescriptor => filterDescriptor.Filter) + .FirstOrDefault(filter => filter is TFilterMetaData); + + return (TFilterMetaData)filterMetadata; + } + + public static ControllerParameterDescriptor GetBodyParameterDescriptor(this ActionDescriptor descriptor) + { + ArgumentGuard.NotNull(descriptor, nameof(descriptor)); + + return (ControllerParameterDescriptor)descriptor.Parameters.FirstOrDefault(parameterDescriptor => + // ReSharper disable once ConstantConditionalAccessQualifier Motivation: see https://github.com/dotnet/aspnetcore/issues/32538 + parameterDescriptor.BindingInfo?.BindingSource == BindingSource.Body); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs new file mode 100644 index 0000000000..b704c698a1 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.OpenApi.JsonApiMetadata; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace JsonApiDotNetCore.OpenApi +{ + /// + /// Adds JsonApiDotNetCore metadata to s if available. This translates to updating response types in + /// and performing an expansion for secondary and relationship endpoints (eg + /// /article/{id}/{relationshipName} -> /article/{id}/author, /article/{id}/revisions, etc). + /// + internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider + { + private readonly IActionDescriptorCollectionProvider _defaultProvider; + private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider; + + public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors(); + + public JsonApiActionDescriptorCollectionProvider(IResourceGraph resourceGraph, 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); + } + + private ActionDescriptorCollection GetActionDescriptors() + { + List newDescriptors = _defaultProvider.ActionDescriptors.Items.ToList(); + List endpoints = newDescriptors.Where(IsVisibleJsonApiEndpoint).ToList(); + + foreach (ActionDescriptor endpoint in endpoints) + { + JsonApiEndpointMetadataContainer endpointMetadataContainer = _jsonApiEndpointMetadataProvider.Get(endpoint.GetActionMethod()); + + List replacementDescriptorsForEndpoint = new(); + replacementDescriptorsForEndpoint.AddRange(AddJsonApiMetadataToAction(endpoint, endpointMetadataContainer.RequestMetadata)); + replacementDescriptorsForEndpoint.AddRange(AddJsonApiMetadataToAction(endpoint, endpointMetadataContainer.ResponseMetadata)); + + if (replacementDescriptorsForEndpoint.Any()) + { + newDescriptors.InsertRange(newDescriptors.IndexOf(endpoint) - 1, replacementDescriptorsForEndpoint); + newDescriptors.Remove(endpoint); + } + } + + int descriptorVersion = _defaultProvider.ActionDescriptors.Version; + return new ActionDescriptorCollection(newDescriptors.AsReadOnly(), descriptorVersion); + } + + private static bool IsVisibleJsonApiEndpoint(ActionDescriptor descriptor) + { + // Only if in a convention ApiExplorer.IsVisible was set to false, the ApiDescriptionActionData will not be present. + return descriptor is ControllerActionDescriptor controllerAction && controllerAction.Properties.ContainsKey(typeof(ApiDescriptionActionData)); + } + + private static IList AddJsonApiMetadataToAction(ActionDescriptor endpoint, IJsonApiEndpointMetadata jsonApiEndpointMetadata) + { + switch (jsonApiEndpointMetadata) + { + case PrimaryResponseMetadata primaryMetadata: + { + UpdateProducesResponseTypeAttribute(endpoint, primaryMetadata.Type); + return Array.Empty(); + } + case PrimaryRequestMetadata primaryMetadata: + { + UpdateBodyParameterDescriptor(endpoint, primaryMetadata.Type); + return Array.Empty(); + } + case ExpansibleEndpointMetadata expansibleMetadata + when expansibleMetadata is RelationshipResponseMetadata || expansibleMetadata is SecondaryResponseMetadata: + { + return Expand(endpoint, expansibleMetadata, + (expandedEndpoint, relationshipType, _) => UpdateProducesResponseTypeAttribute(expandedEndpoint, relationshipType)); + } + case ExpansibleEndpointMetadata expansibleMetadata when expansibleMetadata is RelationshipRequestMetadata: + { + return Expand(endpoint, expansibleMetadata, UpdateBodyParameterDescriptor); + } + default: + { + return Array.Empty(); + } + } + } + + private static void UpdateProducesResponseTypeAttribute(ActionDescriptor endpoint, Type responseTypeToSet) + { + // TODO: is this check really adding anything? is the "else" ever hit, and if so, is that even meaningful? + if (ProducesJsonApiResponseBody(endpoint)) + { + var producesResponse = endpoint.GetFilterMetadata(); + producesResponse.Type = responseTypeToSet; + } + } + + private static bool ProducesJsonApiResponseBody(ActionDescriptor endpoint) + { + var produces = endpoint.GetFilterMetadata(); + + return produces != null && produces.ContentTypes.Any(contentType => contentType == HeaderConstants.MediaType); + } + + private static IList Expand(ActionDescriptor genericEndpoint, ExpansibleEndpointMetadata metadata, + Action expansionCallback) + { + var expansion = new List(); + + foreach ((string relationshipName, Type relationshipType) in metadata.ExpansionElements) + { + ActionDescriptor expandedEndpoint = Clone(genericEndpoint); + RemovePathParameter(expandedEndpoint.Parameters, JsonApiPathParameter.RelationshipName); + ExpandTemplate(expandedEndpoint.AttributeRouteInfo, relationshipName); + + expansionCallback(expandedEndpoint, relationshipType, relationshipName); + + expansion.Add(expandedEndpoint); + } + + return expansion; + } + + private static void UpdateBodyParameterDescriptor(ActionDescriptor endpoint, Type bodyType, string parameterName = null) + { + ControllerParameterDescriptor requestBodyDescriptor = endpoint.GetBodyParameterDescriptor(); + requestBodyDescriptor.ParameterType = bodyType; + ParameterInfo replacementParameterInfo = requestBodyDescriptor.ParameterInfo.WithParameterType(bodyType); + + if (parameterName != null) + { + replacementParameterInfo = replacementParameterInfo.WithName(parameterName); + } + + requestBodyDescriptor.ParameterInfo = replacementParameterInfo; + } + + private static ActionDescriptor Clone(ActionDescriptor descriptor) + { + var clonedDescriptor = (ActionDescriptor)descriptor.MemberwiseClone(); + + clonedDescriptor.AttributeRouteInfo = (AttributeRouteInfo)descriptor.AttributeRouteInfo.MemberwiseClone(); + + clonedDescriptor.FilterDescriptors = new List(); + + foreach (FilterDescriptor filter in descriptor.FilterDescriptors) + { + clonedDescriptor.FilterDescriptors.Add(Clone(filter)); + } + + clonedDescriptor.Parameters = new List(); + + foreach (ParameterDescriptor parameter in descriptor.Parameters) + { + clonedDescriptor.Parameters.Add((ParameterDescriptor)parameter.MemberwiseClone()); + } + + return clonedDescriptor; + } + + private static FilterDescriptor Clone(FilterDescriptor descriptor) + { + var clonedFilter = (IFilterMetadata)descriptor.Filter.MemberwiseClone(); + + return new FilterDescriptor(clonedFilter, descriptor.Scope) + { + Order = descriptor.Order + }; + } + + private static void RemovePathParameter(ICollection parameters, string parameterName) + { + ParameterDescriptor relationshipName = parameters.Single(parameterDescriptor => parameterDescriptor.Name == parameterName); + + parameters.Remove(relationshipName); + } + + private static void ExpandTemplate(AttributeRouteInfo route, string expansionParameter) + { + route.Template = route.Template!.Replace(JsonApiRoutingTemplate.RelationshipNameUrlPlaceholder, expansionParameter); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiEndpoint.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiEndpoint.cs new file mode 100644 index 0000000000..c3ac0cbb95 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiEndpoint.cs @@ -0,0 +1,18 @@ +namespace JsonApiDotNetCore.OpenApi +{ + internal enum JsonApiEndpoint + { + GetCollection, + GetSingle, + GetSecondary, + GetRelationship, + Post, + PostRelationship, + Patch, + PatchRelationship, +#pragma warning disable AV1711 // Name members and local functions similarly to members of .NET Framework classes + Delete, +#pragma warning restore AV1711 // Name members and local functions similarly to members of .NET Framework classes + DeleteRelationship + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiInputFormatterWithMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiInputFormatterWithMetadata.cs new file mode 100644 index 0000000000..b57ee6decd --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiInputFormatterWithMetadata.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Serialization; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Net.Http.Headers; + +namespace JsonApiDotNetCore.OpenApi +{ + internal sealed class JsonApiInputFormatterWithMetadata : IJsonApiInputFormatter, IApiRequestFormatMetadataProvider + { + public bool CanRead(InputFormatterContext context) + { + ArgumentGuard.NotNull(context, nameof(context)); + + return context.HttpContext.IsJsonApiRequest(); + } + + public async Task ReadAsync(InputFormatterContext context) + { + ArgumentGuard.NotNull(context, nameof(context)); + + var reader = context.HttpContext.RequestServices.GetRequiredService(); + return await reader.ReadAsync(context); + } + + public IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) + { + ArgumentGuard.NotNullNorEmpty(contentType, nameof(contentType)); + ArgumentGuard.NotNull(objectType, nameof(objectType)); + + return new MediaTypeCollection + { + new MediaTypeHeaderValue(HeaderConstants.MediaType) + }; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs new file mode 100644 index 0000000000..ee17274a0d --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; + +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal sealed class EndpointResolver + { + public JsonApiEndpoint? Get(MethodInfo controllerAction) + { + ArgumentGuard.NotNull(controllerAction, nameof(controllerAction)); + + HttpMethodAttribute method = controllerAction.GetCustomAttributes(true).OfType().FirstOrDefault(); + + return ResolveJsonApiEndpoint(method); + } + + private static JsonApiEndpoint? ResolveJsonApiEndpoint(HttpMethodAttribute httpMethod) + { + return httpMethod switch + { + HttpGetAttribute attr => attr.Template switch + { + null => JsonApiEndpoint.GetCollection, + JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.GetSingle, + JsonApiRoutingTemplate.SecondaryEndpoint => JsonApiEndpoint.GetSecondary, + JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.GetRelationship, + _ => null + }, + HttpPostAttribute attr => attr.Template switch + { + null => JsonApiEndpoint.Post, + JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.PostRelationship, + _ => null + }, + HttpPatchAttribute attr => attr.Template switch + { + JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.Patch, + JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.PatchRelationship, + _ => null + }, + HttpDeleteAttribute attr => attr.Template switch + { + JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.Delete, + JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.DeleteRelationship, + _ => null + }, + _ => null + }; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/ExpansibleEndpointMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/ExpansibleEndpointMetadata.cs new file mode 100644 index 0000000000..279abddf9c --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/ExpansibleEndpointMetadata.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal abstract class ExpansibleEndpointMetadata + { + public abstract IDictionary ExpansionElements { get; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiEndpointMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiEndpointMetadata.cs new file mode 100644 index 0000000000..a12943366f --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiEndpointMetadata.cs @@ -0,0 +1,6 @@ +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal interface IJsonApiEndpointMetadata + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiRequestMetadata.cs new file mode 100644 index 0000000000..31a5c397be --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiRequestMetadata.cs @@ -0,0 +1,6 @@ +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal interface IJsonApiRequestMetadata : IJsonApiEndpointMetadata + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiResponseMetadata.cs new file mode 100644 index 0000000000..1572daab59 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/IJsonApiResponseMetadata.cs @@ -0,0 +1,6 @@ +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal interface IJsonApiResponseMetadata : IJsonApiEndpointMetadata + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataContainer.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataContainer.cs new file mode 100644 index 0000000000..76a22595bd --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataContainer.cs @@ -0,0 +1,12 @@ +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + /// + /// Metadata available at runtime about a JsonApiDotNetCore endpoint. + /// + internal sealed class JsonApiEndpointMetadataContainer + { + public IJsonApiRequestMetadata RequestMetadata { get; init; } + + public IJsonApiResponseMetadata ResponseMetadata { get; init; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs new file mode 100644 index 0000000000..768098ebc1 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + /// + /// Provides JsonApiDotNetCore related metadata for an ASP.NET controller action that can only be computed from the at + /// runtime. + /// + internal sealed class JsonApiEndpointMetadataProvider + { + private readonly IResourceGraph _resourceGraph; + private readonly IControllerResourceMapping _controllerResourceMapping; + private readonly EndpointResolver _endpointResolver = new(); + + public JsonApiEndpointMetadataProvider(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping) + { + ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph)); + ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); + + _resourceGraph = resourceGraph; + _controllerResourceMapping = controllerResourceMapping; + } + + public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction) + { + ArgumentGuard.NotNull(controllerAction, nameof(controllerAction)); + + JsonApiEndpoint? endpoint = _endpointResolver.Get(controllerAction); + + if (endpoint == null) + { + throw new NotSupportedException($"Unable to provide metadata for non-JsonApiDotNetCore endpoint '{controllerAction.ReflectedType!.FullName}'."); + } + + Type primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(controllerAction.ReflectedType); + + return new JsonApiEndpointMetadataContainer + { + RequestMetadata = GetRequestMetadata(endpoint.Value, primaryResourceType), + ResponseMetadata = GetResponseMetadata(endpoint.Value, primaryResourceType) + }; + } + + private IJsonApiRequestMetadata GetRequestMetadata(JsonApiEndpoint endpoint, Type primaryResourceType) + { + switch (endpoint) + { + case JsonApiEndpoint.Post: + { + return GetPostRequestMetadata(primaryResourceType); + } + case JsonApiEndpoint.Patch: + { + return GetPatchRequestMetadata(primaryResourceType); + } + case JsonApiEndpoint.PostRelationship: + case JsonApiEndpoint.PatchRelationship: + case JsonApiEndpoint.DeleteRelationship: + { + return GetRelationshipRequestMetadata(primaryResourceType, endpoint != JsonApiEndpoint.PatchRelationship); + } + default: + { + return null; + } + } + } + + private static PrimaryRequestMetadata GetPostRequestMetadata(Type primaryResourceType) + { + return new() + { + Type = typeof(ResourcePostRequestDocument<>).MakeGenericType(primaryResourceType) + }; + } + + private static PrimaryRequestMetadata GetPatchRequestMetadata(Type primaryResourceType) + { + return new() + { + Type = typeof(ResourcePatchRequestDocument<>).MakeGenericType(primaryResourceType) + }; + } + + private RelationshipRequestMetadata GetRelationshipRequestMetadata(Type primaryResourceType, bool ignoreHasOneRelationships) + { + IEnumerable relationships = _resourceGraph.GetResourceContext(primaryResourceType).Relationships; + + if (ignoreHasOneRelationships) + { + relationships = relationships.OfType(); + } + + IDictionary resourceTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, + relationship => relationship is HasManyAttribute + ? typeof(ToManyRelationshipRequestData<>).MakeGenericType(relationship.RightType) + : typeof(ToOneRelationshipRequestData<>).MakeGenericType(relationship.RightType)); + + return new RelationshipRequestMetadata + { + RequestBodyTypeByRelationshipName = resourceTypesByRelationshipName + }; + } + + private IJsonApiResponseMetadata GetResponseMetadata(JsonApiEndpoint endpoint, Type primaryResourceType) + { + switch (endpoint) + { + case JsonApiEndpoint.GetCollection: + case JsonApiEndpoint.GetSingle: + case JsonApiEndpoint.Post: + case JsonApiEndpoint.Patch: + { + return GetPrimaryResponseMetadata(primaryResourceType, endpoint == JsonApiEndpoint.GetCollection); + } + case JsonApiEndpoint.GetSecondary: + { + return GetSecondaryResponseMetadata(primaryResourceType); + } + case JsonApiEndpoint.GetRelationship: + { + return GetRelationshipResponseMetadata(primaryResourceType); + } + default: + { + return null; + } + } + } + + private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type primaryResourceType, bool endpointReturnsCollection) + { + Type documentType = endpointReturnsCollection ? typeof(ResourceCollectionResponseDocument<>) : typeof(PrimaryResourceResponseDocument<>); + + return new PrimaryResponseMetadata + { + Type = documentType.MakeGenericType(primaryResourceType) + }; + } + + private SecondaryResponseMetadata GetSecondaryResponseMetadata(Type primaryResourceType) + { + IDictionary responseTypesByRelationshipName = GetMetadataByRelationshipName(primaryResourceType, relationship => + { + Type documentType = relationship is HasManyAttribute + ? typeof(ResourceCollectionResponseDocument<>) + : typeof(SecondaryResourceResponseDocument<>); + + return documentType.MakeGenericType(relationship.RightType); + }); + + return new SecondaryResponseMetadata + { + ResponseTypesByRelationshipName = responseTypesByRelationshipName + }; + } + + private IDictionary GetMetadataByRelationshipName(Type primaryResourceType, + Func extractRelationshipMetadataCallback) + { + IReadOnlyCollection relationships = _resourceGraph.GetResourceContext(primaryResourceType).Relationships; + + return relationships.ToDictionary(relationship => relationship.PublicName, extractRelationshipMetadataCallback); + } + + private RelationshipResponseMetadata GetRelationshipResponseMetadata(Type primaryResourceType) + { + IDictionary responseTypesByRelationshipName = GetMetadataByRelationshipName(primaryResourceType, + relationship => relationship is HasManyAttribute + ? typeof(ResourceIdentifierCollectionResponseDocument<>).MakeGenericType(relationship.RightType) + : typeof(ResourceIdentifierResponseDocument<>).MakeGenericType(relationship.RightType)); + + return new RelationshipResponseMetadata + { + ResponseTypesByRelationshipName = responseTypesByRelationshipName + }; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/PrimaryRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/PrimaryRequestMetadata.cs new file mode 100644 index 0000000000..c217aefcb3 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/PrimaryRequestMetadata.cs @@ -0,0 +1,9 @@ +using System; + +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal sealed class PrimaryRequestMetadata : IJsonApiRequestMetadata + { + public Type Type { get; init; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/PrimaryResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/PrimaryResponseMetadata.cs new file mode 100644 index 0000000000..13647ec857 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/PrimaryResponseMetadata.cs @@ -0,0 +1,9 @@ +using System; + +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal sealed class PrimaryResponseMetadata : IJsonApiResponseMetadata + { + public Type Type { get; init; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipRequestMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipRequestMetadata.cs new file mode 100644 index 0000000000..9156803a3b --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipRequestMetadata.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal sealed class RelationshipRequestMetadata : ExpansibleEndpointMetadata, IJsonApiRequestMetadata + { + public IDictionary RequestBodyTypeByRelationshipName { get; init; } + + public override IDictionary ExpansionElements => RequestBodyTypeByRelationshipName; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipResponseMetadata.cs new file mode 100644 index 0000000000..28b9cd2df1 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/RelationshipResponseMetadata.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal sealed class RelationshipResponseMetadata : ExpansibleEndpointMetadata, IJsonApiResponseMetadata + { + public IDictionary ResponseTypesByRelationshipName { get; init; } + + public override IDictionary ExpansionElements => ResponseTypesByRelationshipName; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/SecondaryResponseMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/SecondaryResponseMetadata.cs new file mode 100644 index 0000000000..45e5f4e0ab --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/SecondaryResponseMetadata.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata +{ + internal sealed class SecondaryResponseMetadata : ExpansibleEndpointMetadata, IJsonApiResponseMetadata + { + public IDictionary ResponseTypesByRelationshipName { get; init; } + + public override IDictionary ExpansionElements => ResponseTypesByRelationshipName; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjectPropertyName.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjectPropertyName.cs new file mode 100644 index 0000000000..c9ff79f9d6 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjectPropertyName.cs @@ -0,0 +1,19 @@ +#pragma warning disable AV1008 // Class should not be static + +namespace JsonApiDotNetCore.OpenApi +{ + internal static class JsonApiObjectPropertyName + { + public const string Type = "type"; + public const string Id = "id"; + public const string Data = "data"; + public const string AttributesObject = "attributes"; + public const string RelationshipsObject = "relationships"; + public const string MetaObject = "meta"; + public const string LinksObject = "links"; + public const string JsonapiObject = "jsonapi"; + public const string JsonapiObjectVersion = "version"; + public const string JsonapiObjectExt = "ext"; + public const string JsonapiObjectProfile = "profile"; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs new file mode 100644 index 0000000000..931f787808 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs @@ -0,0 +1,21 @@ +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.Documents +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class PrimaryResourceResponseDocument : SingleData> + where TResource : IIdentifiable + { + public IDictionary Meta { get; set; } + + public JsonapiObject Jsonapi { get; set; } + + [Required] + public LinksInResourceDocument Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs new file mode 100644 index 0000000000..a6a2377bdc --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs @@ -0,0 +1,21 @@ +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.Documents +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ResourceCollectionResponseDocument : ManyData> + where TResource : IIdentifiable + { + public IDictionary Meta { get; set; } + + public JsonapiObject Jsonapi { get; set; } + + [Required] + public LinksInResourceCollectionDocument Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs new file mode 100644 index 0000000000..85e9b9e7f0 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs @@ -0,0 +1,21 @@ +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.Documents +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ResourceIdentifierCollectionResponseDocument : ManyData> + where TResource : IIdentifiable + { + public IDictionary Meta { get; set; } + + public JsonapiObject Jsonapi { get; set; } + + [Required] + public LinksInResourceIdentifierCollectionDocument Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs new file mode 100644 index 0000000000..b1b4299191 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs @@ -0,0 +1,21 @@ +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.Documents +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ResourceIdentifierResponseDocument : SingleData> + where TResource : IIdentifiable + { + public IDictionary Meta { get; set; } + + public JsonapiObject Jsonapi { get; set; } + + [Required] + public LinksInResourceIdentifierDocument Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs new file mode 100644 index 0000000000..7246695ccd --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ResourcePatchRequestDocument : SingleData> + where TResource : IIdentifiable + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs new file mode 100644 index 0000000000..034863b5cf --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ResourcePostRequestDocument : SingleData> + where TResource : IIdentifiable + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs new file mode 100644 index 0000000000..876e290565 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs @@ -0,0 +1,21 @@ +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.Documents +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class SecondaryResourceResponseDocument : SingleData> + where TResource : IIdentifiable + { + public IDictionary Meta { get; set; } + + public JsonapiObject Jsonapi { get; set; } + + [Required] + public LinksInResourceDocument Links { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonapiObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonapiObject.cs new file mode 100644 index 0000000000..6f7d7bd4fa --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonapiObject.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class JsonapiObject + { + public string Version { get; set; } + + public ICollection Ext { get; set; } + + public ICollection Profile { get; set; } + + public IDictionary Meta { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationshipObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationshipObject.cs new file mode 100644 index 0000000000..8b1ca67162 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationshipObject.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class LinksInRelationshipObject + { + [Required] + public string Self { get; set; } + + [Required] + public string Related { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceCollectionDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceCollectionDocument.cs new file mode 100644 index 0000000000..84d5e37aa0 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceCollectionDocument.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class LinksInResourceCollectionDocument + { + [Required] + public string Self { get; set; } + + public string Describedby { get; set; } + + [Required] + public string First { get; set; } + + public string Last { get; set; } + + public string Prev { get; set; } + + public string Next { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceDocument.cs new file mode 100644 index 0000000000..f2686c12b3 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceDocument.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class LinksInResourceDocument + { + [Required] + public string Self { get; set; } + + public string Describedby { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceIdentifierCollectionDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceIdentifierCollectionDocument.cs new file mode 100644 index 0000000000..8596f60156 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceIdentifierCollectionDocument.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class LinksInResourceIdentifierCollectionDocument + { + [Required] + public string Self { get; set; } + + public string Describedby { get; set; } + + [Required] + public string Related { get; set; } + + [Required] + public string First { get; set; } + + public string Last { get; set; } + + public string Prev { get; set; } + + public string Next { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceIdentifierDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceIdentifierDocument.cs new file mode 100644 index 0000000000..88d568f648 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceIdentifierDocument.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class LinksInResourceIdentifierDocument + { + [Required] + public string Self { get; set; } + + public string Describedby { get; set; } + + [Required] + public string Related { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceObject.cs new file mode 100644 index 0000000000..10313617cf --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceObject.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class LinksInResourceObject + { + [Required] + public string Self { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs new file mode 100644 index 0000000000..b6253f5142 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal abstract class ManyData + where TData : ResourceIdentifierObject + { + [Required] + public ICollection Data { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToManyRelationshipRequestData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToManyRelationshipRequestData.cs new file mode 100644 index 0000000000..0d29dd496a --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToManyRelationshipRequestData.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 ToManyRelationshipRequestData : ManyData> + where TResource : IIdentifiable + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToManyRelationshipResponseData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToManyRelationshipResponseData.cs new file mode 100644 index 0000000000..a6f10e9e9a --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToManyRelationshipResponseData.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 ToManyRelationshipResponseData : ManyData> + where TResource : IIdentifiable + { + [Required] + public LinksInRelationshipObject Links { get; set; } + + public IDictionary Meta { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToOneRelationshipRequestData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToOneRelationshipRequestData.cs new file mode 100644 index 0000000000..1526768c31 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToOneRelationshipRequestData.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 ToOneRelationshipRequestData : SingleData> + where TResource : IIdentifiable + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToOneRelationshipResponseData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToOneRelationshipResponseData.cs new file mode 100644 index 0000000000..5edc6b3450 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/RelationshipData/ToOneRelationshipResponseData.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 ToOneRelationshipResponseData : SingleData> + where TResource : IIdentifiable + { + [Required] + public LinksInRelationshipObject Links { get; set; } + + public IDictionary Meta { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifierObject.cs new file mode 100644 index 0000000000..e2f6d08136 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifierObject.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects +{ + // ReSharper disable once UnusedTypeParameter + internal class ResourceIdentifierObject : ResourceIdentifierObject + where TResource : IIdentifiable + { + } + + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal class ResourceIdentifierObject + { + [Required] + public string Type { get; set; } + + [Required] + public string Id { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObject.cs new file mode 100644 index 0000000000..80366a8277 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObject.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal abstract class ResourceObject : ResourceIdentifierObject + where TResource : IIdentifiable + { + public IDictionary Attributes { get; set; } + + public IDictionary Relationships { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourcePatchRequestObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourcePatchRequestObject.cs new file mode 100644 index 0000000000..e642d93bbb --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourcePatchRequestObject.cs @@ -0,0 +1,9 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects +{ + internal sealed class ResourcePatchRequestObject : ResourceObject + where TResource : IIdentifiable + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourcePostRequestObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourcePostRequestObject.cs new file mode 100644 index 0000000000..9447c74668 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourcePostRequestObject.cs @@ -0,0 +1,9 @@ +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects +{ + internal sealed class ResourcePostRequestObject : ResourceObject + where TResource : IIdentifiable + { + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceResponseObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceResponseObject.cs new file mode 100644 index 0000000000..44fcfdcfe0 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceResponseObject.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links; +using JsonApiDotNetCore.Resources; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal sealed class ResourceResponseObject : ResourceObject + where TResource : IIdentifiable + { + [Required] + public LinksInResourceObject Links { get; set; } + + public IDictionary Meta { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs new file mode 100644 index 0000000000..616f357014 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + internal abstract class SingleData + where TData : ResourceIdentifierObject + { + [Required] + public TData Data { get; set; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs new file mode 100644 index 0000000000..a880d6ce5c --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; +using JsonApiDotNetCore.Middleware; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Newtonsoft.Json.Serialization; + +namespace JsonApiDotNetCore.OpenApi +{ + internal sealed class JsonApiOperationIdSelector + { + private const string ResourceOperationIdTemplate = "[Method] [PrimaryResourceName]"; + private const string ResourceCollectionOperationIdTemplate = ResourceOperationIdTemplate + " Collection"; + private const string SecondaryOperationIdTemplate = ResourceOperationIdTemplate + " [RelationshipName]"; + private const string RelationshipOperationIdTemplate = SecondaryOperationIdTemplate + " Relationship"; + + private static readonly IDictionary DocumentOpenTypeToOperationIdTemplateMap = new Dictionary + { + [typeof(ResourceCollectionResponseDocument<>)] = ResourceCollectionOperationIdTemplate, + [typeof(PrimaryResourceResponseDocument<>)] = ResourceOperationIdTemplate, + [typeof(ResourcePostRequestDocument<>)] = ResourceOperationIdTemplate, + [typeof(ResourcePatchRequestDocument<>)] = ResourceOperationIdTemplate, + [typeof(void)] = ResourceOperationIdTemplate, + [typeof(SecondaryResourceResponseDocument<>)] = SecondaryOperationIdTemplate, + [typeof(ResourceIdentifierCollectionResponseDocument<>)] = RelationshipOperationIdTemplate, + [typeof(ResourceIdentifierResponseDocument<>)] = RelationshipOperationIdTemplate, + [typeof(ToOneRelationshipRequestData<>)] = RelationshipOperationIdTemplate, + [typeof(ToManyRelationshipRequestData<>)] = RelationshipOperationIdTemplate + }; + + private readonly IControllerResourceMapping _controllerResourceMapping; + private readonly NamingStrategy _namingStrategy; + private readonly ResourceNameFormatterProxy _formatter; + + public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, NamingStrategy namingStrategy) + { + ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); + ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); + + _controllerResourceMapping = controllerResourceMapping; + _namingStrategy = namingStrategy; + _formatter = new ResourceNameFormatterProxy(namingStrategy); + } + + public string GetOperationId(ApiDescription endpoint) + { + ArgumentGuard.NotNull(endpoint, nameof(endpoint)); + + Type primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(endpoint.ActionDescriptor.GetActionMethod().ReflectedType); + + string template = GetTemplate(primaryResourceType, endpoint); + + return ApplyTemplate(template, primaryResourceType, endpoint); + } + + private static string GetTemplate(Type primaryResourceType, ApiDescription endpoint) + { + Type requestDocumentType = GetDocumentType(primaryResourceType, endpoint); + + return DocumentOpenTypeToOperationIdTemplateMap[requestDocumentType]; + } + + private static Type GetDocumentType(Type primaryResourceType, ApiDescription endpoint) + { + ControllerParameterDescriptor requestBodyDescriptor = endpoint.ActionDescriptor.GetBodyParameterDescriptor(); + var producesResponseTypeAttribute = endpoint.ActionDescriptor.GetFilterMetadata(); + + Type documentType = requestBodyDescriptor?.ParameterType.GetGenericTypeDefinition() ?? + TryGetGenericTypeDefinition(producesResponseTypeAttribute.Type) ?? producesResponseTypeAttribute.Type; + + if (documentType == typeof(ResourceCollectionResponseDocument<>)) + { + Type documentResourceType = producesResponseTypeAttribute.Type.GetGenericArguments()[0]; + + if (documentResourceType != primaryResourceType) + { + documentType = typeof(SecondaryResourceResponseDocument<>); + } + } + + return documentType; + } + + private static Type TryGetGenericTypeDefinition(Type type) + { + return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null; + } + + private string ApplyTemplate(string operationIdTemplate, Type primaryResourceType, ApiDescription endpoint) + { + string method = endpoint.HttpMethod!.ToLowerInvariant(); + string primaryResourceName = _formatter.FormatResourceName(primaryResourceType).Singularize(); + string relationshipName = operationIdTemplate.Contains("[RelationshipName]") ? endpoint.RelativePath.Split("/").Last() : string.Empty; + + // @formatter:wrap_chained_method_calls chop_always + // @formatter:keep_existing_linebreaks true + + string pascalCaseId = operationIdTemplate + .Replace("[Method]", method) + .Replace("[PrimaryResourceName]", primaryResourceName) + .Replace("[RelationshipName]", relationshipName); + + // @formatter:keep_existing_linebreaks restore + // @formatter:wrap_chained_method_calls restore + + return _namingStrategy.GetPropertyName(pascalCaseId, false); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiPathParameter.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiPathParameter.cs new file mode 100644 index 0000000000..497ba9faa7 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiPathParameter.cs @@ -0,0 +1,9 @@ +#pragma warning disable AV1008 // Class should not be static + +namespace JsonApiDotNetCore.OpenApi +{ + internal static class JsonApiPathParameter + { + public const string RelationshipName = "relationshipName"; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiRoutingTemplate.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiRoutingTemplate.cs new file mode 100644 index 0000000000..aff8ed85a8 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiRoutingTemplate.cs @@ -0,0 +1,13 @@ +#pragma warning disable AV1008 // Class should not be static + +namespace JsonApiDotNetCore.OpenApi +{ + internal static class JsonApiRoutingTemplate + { + public const string RelationshipNameUrlPlaceholder = "{" + JsonApiPathParameter.RelationshipName + "}"; + public const string RelationshipsPart = "relationships"; + public const string PrimaryEndpoint = "{id}"; + public const string SecondaryEndpoint = "{id}/" + RelationshipNameUrlPlaceholder; + public const string RelationshipEndpoint = "{id}/" + RelationshipsPart + "/" + RelationshipNameUrlPlaceholder; + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs new file mode 100644 index 0000000000..9ded9e05b4 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Humanizer; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; +using JsonApiDotNetCore.Configuration; + +namespace JsonApiDotNetCore.OpenApi +{ + internal sealed class JsonApiSchemaIdSelector + { + private static readonly IDictionary OpenTypeToSchemaTemplateMap = new Dictionary + { + [typeof(ResourcePostRequestDocument<>)] = "###-post-request-document", + [typeof(ResourcePatchRequestDocument<>)] = "###-patch-request-document", + [typeof(ResourcePostRequestObject<>)] = "###-data-in-post-request", + [typeof(ResourcePatchRequestObject<>)] = "###-data-in-patch-request", + [typeof(ToOneRelationshipRequestData<>)] = "to-one-###-request-data", + [typeof(ToManyRelationshipRequestData<>)] = "to-many-###-request-data", + [typeof(PrimaryResourceResponseDocument<>)] = "###-primary-response-document", + [typeof(SecondaryResourceResponseDocument<>)] = "###-secondary-response-document", + [typeof(ResourceCollectionResponseDocument<>)] = "###-collection-response-document", + [typeof(ResourceIdentifierResponseDocument<>)] = "###-identifier-response-document", + [typeof(ResourceIdentifierCollectionResponseDocument<>)] = "###-identifier-collection-response-document", + [typeof(ToOneRelationshipResponseData<>)] = "to-one-###-response-data", + [typeof(ToManyRelationshipResponseData<>)] = "to-many-###-response-data", + [typeof(ResourceResponseObject<>)] = "###-data-in-response", + [typeof(ResourceIdentifierObject<>)] = "###-identifier" + }; + + private readonly ResourceNameFormatterProxy _formatter; + private readonly IResourceContextProvider _resourceContextProvider; + + public JsonApiSchemaIdSelector(ResourceNameFormatterProxy formatter, IResourceContextProvider resourceContextProvider) + { + ArgumentGuard.NotNull(formatter, nameof(formatter)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + _formatter = formatter; + _resourceContextProvider = resourceContextProvider; + } + + public string GetSchemaId(Type type) + { + ArgumentGuard.NotNull(type, nameof(type)); + + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(type); + + if (resourceContext != null) + { + return resourceContext.PublicName.Singularize(); + } + + if (type.IsConstructedGenericType && OpenTypeToSchemaTemplateMap.ContainsKey(type.GetGenericTypeDefinition())) + { + Type resourceType = type.GetGenericArguments().First(); + string resourceName = _formatter.FormatResourceName(resourceType).Singularize(); + + string template = OpenTypeToSchemaTemplateMap[type.GetGenericTypeDefinition()]; + return template.Replace("###", resourceName); + } + + // Used for a fixed set of types, such as jsonapi-object, links-in-many-resource-document etc. + return _formatter.FormatResourceName(type).Singularize(); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ObjectExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ObjectExtensions.cs new file mode 100644 index 0000000000..573067520b --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/ObjectExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Reflection; +using System.Threading; + +namespace JsonApiDotNetCore.OpenApi +{ + internal static class ObjectExtensions + { + private static readonly Lazy MemberwiseCloneMethod = new(() => + typeof(object).GetMethod(nameof(MemberwiseClone), BindingFlags.Instance | BindingFlags.NonPublic), LazyThreadSafetyMode.ExecutionAndPublication); + + public static object MemberwiseClone(this object source) + { + ArgumentGuard.NotNull(source, nameof(source)); + + return MemberwiseCloneMethod.Value.Invoke(source, null); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs index a9b58bdd16..89829d5d25 100644 --- a/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiEndpointConvention.cs @@ -1,19 +1,170 @@ +using System; +using System.Collections.Generic; using System.Linq; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.OpenApi.JsonApiMetadata; +using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; -using Microsoft.AspNetCore.Mvc.Routing; namespace JsonApiDotNetCore.OpenApi { + /// + /// Sets metadata on controllers for OpenAPI documentation generation by Swagger. Only targets JsonApiDotNetCore controllers. + /// internal sealed class OpenApiEndpointConvention : IActionModelConvention { + private readonly IResourceContextProvider _resourceContextProvider; + private readonly IControllerResourceMapping _controllerResourceMapping; + private readonly EndpointResolver _endpointResolver = new(); + + public OpenApiEndpointConvention(IResourceContextProvider resourceContextProvider, IControllerResourceMapping controllerResourceMapping) + { + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping)); + + _resourceContextProvider = resourceContextProvider; + _controllerResourceMapping = controllerResourceMapping; + } + public void Apply(ActionModel action) { ArgumentGuard.NotNull(action, nameof(action)); - if (!action.ActionMethod.GetCustomAttributes(true).OfType().Any()) + JsonApiEndpoint? endpoint = _endpointResolver.Get(action.ActionMethod); + + if (endpoint == null || ShouldSuppressEndpoint(endpoint.Value, action.Controller.ControllerType)) { action.ApiExplorer.IsVisible = false; + + return; } + + SetResponseMetadata(action, endpoint.Value); + + SetRequestMetadata(action, endpoint.Value); + } + + private bool ShouldSuppressEndpoint(JsonApiEndpoint endpoint, Type controllerType) + { + if (IsSecondaryOrRelationshipEndpoint(endpoint)) + { + IReadOnlyCollection relationships = GetRelationshipsOfPrimaryResource(controllerType); + + if (!relationships.Any()) + { + return true; + } + + if (endpoint == JsonApiEndpoint.DeleteRelationship || endpoint == JsonApiEndpoint.PostRelationship) + { + return !relationships.OfType().Any(); + } + } + + return false; + } + + private IReadOnlyCollection GetRelationshipsOfPrimaryResource(Type controllerType) + { + Type primaryResourceOfEndpointType = _controllerResourceMapping.GetResourceTypeForController(controllerType); + + ResourceContext primaryResourceContext = _resourceContextProvider.GetResourceContext(primaryResourceOfEndpointType); + + return primaryResourceContext.Relationships; + } + + private static bool IsSecondaryOrRelationshipEndpoint(JsonApiEndpoint endpoint) + { + return endpoint == JsonApiEndpoint.GetSecondary || endpoint == JsonApiEndpoint.GetRelationship || endpoint == JsonApiEndpoint.PostRelationship || + endpoint == JsonApiEndpoint.PatchRelationship || endpoint == JsonApiEndpoint.DeleteRelationship; + } + + private void SetResponseMetadata(ActionModel action, JsonApiEndpoint endpoint) + { + IList statusCodes = GetStatusCodesForEndpoint(endpoint); + + foreach (int statusCode in statusCodes) + { + action.Filters.Add(new ProducesResponseTypeAttribute(statusCode)); + + switch (endpoint) + { + case JsonApiEndpoint.GetCollection when statusCode == StatusCodes.Status200OK: + case JsonApiEndpoint.Post when statusCode == StatusCodes.Status201Created: + case JsonApiEndpoint.Patch when statusCode == StatusCodes.Status200OK: + case JsonApiEndpoint.GetSingle when statusCode == StatusCodes.Status200OK: + case JsonApiEndpoint.GetSecondary when statusCode == StatusCodes.Status200OK: + case JsonApiEndpoint.GetRelationship when statusCode == StatusCodes.Status200OK: + { + action.Filters.Add(new ProducesAttribute(HeaderConstants.MediaType)); + break; + } + } + } + } + + private static IList GetStatusCodesForEndpoint(JsonApiEndpoint endpoint) + { + switch (endpoint) + { + case JsonApiEndpoint.GetCollection: + case JsonApiEndpoint.GetSingle: + case JsonApiEndpoint.GetSecondary: + case JsonApiEndpoint.GetRelationship: + { + return new[] + { + StatusCodes.Status200OK + }; + } + case JsonApiEndpoint.Post: + { + return new[] + { + StatusCodes.Status201Created, + StatusCodes.Status204NoContent + }; + } + case JsonApiEndpoint.Patch: + { + return new[] + { + StatusCodes.Status200OK, + StatusCodes.Status204NoContent + }; + } + case JsonApiEndpoint.Delete: + case JsonApiEndpoint.PostRelationship: + case JsonApiEndpoint.PatchRelationship: + case JsonApiEndpoint.DeleteRelationship: + { + return new[] + { + StatusCodes.Status204NoContent + }; + } + default: + { + throw new UnreachableCodeException(); + } + } + } + + private static void SetRequestMetadata(ActionModel action, JsonApiEndpoint endpoint) + { + if (RequiresRequestBody(endpoint)) + { + action.Filters.Add(new ConsumesAttribute(HeaderConstants.MediaType)); + } + } + + private static bool RequiresRequestBody(JsonApiEndpoint endpoint) + { + return endpoint is JsonApiEndpoint.Post || endpoint is JsonApiEndpoint.Patch || endpoint is JsonApiEndpoint.PostRelationship || + endpoint is JsonApiEndpoint.PatchRelationship || endpoint is JsonApiEndpoint.DeleteRelationship; } } } diff --git a/src/JsonApiDotNetCore.OpenApi/ParameterInfoExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ParameterInfoExtensions.cs new file mode 100644 index 0000000000..9d46706586 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/ParameterInfoExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Reflection; +using System.Threading; + +namespace JsonApiDotNetCore.OpenApi +{ + internal static class ParameterInfoExtensions + { + private static readonly Lazy NameField = new(() => + typeof(ParameterInfo).GetField("NameImpl", BindingFlags.Instance | BindingFlags.NonPublic), LazyThreadSafetyMode.ExecutionAndPublication); + + private static readonly Lazy ParameterTypeField = new(() => + typeof(ParameterInfo).GetField("ClassImpl", BindingFlags.Instance | BindingFlags.NonPublic), LazyThreadSafetyMode.ExecutionAndPublication); + + public static ParameterInfo WithName(this ParameterInfo source, string name) + { + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNullNorEmpty(name, nameof(name)); + + var cloned = (ParameterInfo)source.MemberwiseClone(); + NameField.Value.SetValue(cloned, name); + + return cloned; + } + + public static ParameterInfo WithParameterType(this ParameterInfo source, Type parameterType) + { + ArgumentGuard.NotNull(source, nameof(source)); + ArgumentGuard.NotNull(parameterType, nameof(parameterType)); + + var cloned = (ParameterInfo)source.MemberwiseClone(); + ParameterTypeField.Value.SetValue(cloned, parameterType); + + return cloned; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceNameFormatterProxy.cs b/src/JsonApiDotNetCore.OpenApi/ResourceNameFormatterProxy.cs new file mode 100644 index 0000000000..c291ba36f6 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/ResourceNameFormatterProxy.cs @@ -0,0 +1,50 @@ +using System; +using System.Reflection; +using JsonApiDotNetCore.Resources; +using Newtonsoft.Json.Serialization; + +namespace JsonApiDotNetCore.OpenApi +{ + internal sealed class ResourceNameFormatterProxy + { + private const string ResourceNameFormatterTypeName = "JsonApiDotNetCore.Configuration.ResourceNameFormatter"; + private const string FormatResourceNameMethodName = "FormatResourceName"; + + private readonly NamingStrategy _namingStrategy; + private readonly Type _resourceNameFormatterType; + private readonly MethodInfo _formatResourceNameMethod; + + public ResourceNameFormatterProxy(NamingStrategy namingStrategy) + { + ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); + + _namingStrategy = namingStrategy; + + _resourceNameFormatterType = typeof(IIdentifiable).Assembly.GetType(ResourceNameFormatterTypeName); + + if (_resourceNameFormatterType == null) + { + throw new InvalidOperationException($"Failed to locate '{ResourceNameFormatterTypeName}'."); + } + + _formatResourceNameMethod = _resourceNameFormatterType.GetMethod(FormatResourceNameMethodName); + + if (_formatResourceNameMethod == null) + { + throw new InvalidOperationException($"Failed to locate '{ResourceNameFormatterTypeName}.{FormatResourceNameMethodName}'."); + } + } + + public string FormatResourceName(Type type) + { + ArgumentGuard.NotNull(type, nameof(type)); + + object resourceNameFormatter = Activator.CreateInstance(_resourceNameFormatterType, _namingStrategy); + + return (string)_formatResourceNameMethod.Invoke(resourceNameFormatter, new object[] + { + type + }); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index e94d7458cf..562e15fb58 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -1,5 +1,15 @@ using System; +using System.Collections.Generic; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.OpenApi.SwaggerComponents; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Serialization; +using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; namespace JsonApiDotNetCore.OpenApi @@ -14,11 +24,120 @@ public static void AddOpenApi(this IServiceCollection services, IMvcCoreBuilder ArgumentGuard.NotNull(services, nameof(services)); ArgumentGuard.NotNull(mvcBuilder, nameof(mvcBuilder)); + AddCustomApiExplorer(services, mvcBuilder); + + AddCustomSwaggerComponents(services); + + using ServiceProvider provider = services.BuildServiceProvider(); + using IServiceScope scope = provider.CreateScope(); + AddSwaggerGenerator(scope, services, setupSwaggerGenAction); + AddSwashbuckleCliCompatibility(scope, mvcBuilder); + AddOpenApiEndpointConvention(scope, mvcBuilder); + + AddJsonApiInputFormatterWorkaround(mvcBuilder); + } + + private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBuilder mvcBuilder) + { + 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); + + return new ApiDescriptionGroupCollectionProvider(descriptorCollectionProviderWrapper, apiDescriptionProviders); + }); + mvcBuilder.AddApiExplorer(); + } + + private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection services, Action setupSwaggerGenAction) + { + var controllerResourceMapping = scope.ServiceProvider.GetRequiredService(); + var resourceContextProvider = scope.ServiceProvider.GetRequiredService(); + var jsonApiOptions = scope.ServiceProvider.GetRequiredService(); + NamingStrategy namingStrategy = ((DefaultContractResolver)jsonApiOptions.SerializerSettings.ContractResolver)!.NamingStrategy; + + AddSchemaGenerator(services); + + services.AddSwaggerGen(swaggerGenOptions => + { + SetOperationInfo(swaggerGenOptions, controllerResourceMapping, resourceContextProvider, namingStrategy); + SetSchemaIdSelector(swaggerGenOptions, resourceContextProvider, namingStrategy); + swaggerGenOptions.DocumentFilter(); + + setupSwaggerGenAction?.Invoke(swaggerGenOptions); + }); + } + + private static void AddSchemaGenerator(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + } + + private static void SetOperationInfo(SwaggerGenOptions swaggerGenOptions, IControllerResourceMapping controllerResourceMapping, + IResourceContextProvider resourceContextProvider, NamingStrategy namingStrategy) + { + swaggerGenOptions.TagActionsBy(description => GetOperationTags(description, controllerResourceMapping, resourceContextProvider)); - mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention())); + JsonApiOperationIdSelector jsonApiOperationIdSelector = new(controllerResourceMapping, namingStrategy); + swaggerGenOptions.CustomOperationIds(jsonApiOperationIdSelector.GetOperationId); + } + + private static IList GetOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping, + IResourceContextProvider resourceContextProvider) + { + MethodInfo actionMethod = description.ActionDescriptor.GetActionMethod(); + Type resourceType = controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType); + ResourceContext resourceContext = resourceContextProvider.GetResourceContext(resourceType); + + return new[] + { + resourceContext.PublicName + }; + } + + private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceContextProvider resourceContextProvider, + NamingStrategy namingStrategy) + { + ResourceNameFormatterProxy resourceNameFormatter = new(namingStrategy); + JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(resourceNameFormatter, resourceContextProvider); + + swaggerGenOptions.CustomSchemaIds(type => jsonApiObjectSchemaSelector.GetSchemaId(type)); + } + + private static void AddCustomSwaggerComponents(IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); - services.AddSwaggerGen(setupSwaggerGenAction); + services.AddSingleton(); + } + + private static void AddSwashbuckleCliCompatibility(IServiceScope scope, IMvcCoreBuilder mvcBuilder) + { + // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1957 for why this is needed. + var routingConvention = scope.ServiceProvider.GetRequiredService(); + mvcBuilder.AddMvcOptions(options => options.Conventions.Insert(0, routingConvention)); + } + + private static void AddOpenApiEndpointConvention(IServiceScope scope, IMvcCoreBuilder mvcBuilder) + { + var resourceContextProvider = scope.ServiceProvider.GetRequiredService(); + var controllerResourceMapping = scope.ServiceProvider.GetRequiredService(); + + mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(resourceContextProvider, controllerResourceMapping))); + } + + private static void AddJsonApiInputFormatterWorkaround(IMvcCoreBuilder mvcBuilder) + { + // See https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/972 for why this is needed. + mvcBuilder.AddMvcOptions(options => options.InputFormatters.Add(new JsonApiInputFormatterWithMetadata())); } } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs new file mode 100644 index 0000000000..0a5d1180b3 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/CachingSwaggerGenerator.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.Swagger; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + /// + /// The default implementation re-renders the OpenApiDocument every time it is requested, which is redundant in our case. + /// This implementation provides a very basic caching layer. + /// + internal sealed class CachingSwaggerGenerator : ISwaggerProvider + { + private readonly SwaggerGenerator _defaultSwaggerGenerator; + private readonly ConcurrentDictionary _openApiDocumentCache = new(); + + public CachingSwaggerGenerator(SwaggerGenerator defaultSwaggerGenerator) + { + ArgumentGuard.NotNull(defaultSwaggerGenerator, nameof(defaultSwaggerGenerator)); + _defaultSwaggerGenerator = defaultSwaggerGenerator; + } + + public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null) + { + ArgumentGuard.NotNullNorEmpty(documentName, nameof(documentName)); + + string cacheKey = $"{documentName}#{host}#{basePath}"; + + return _openApiDocumentCache.GetOrAdd(cacheKey, _ => _defaultSwaggerGenerator.GetSwagger(documentName, host, basePath)); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/EndpointOrderingFilter.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/EndpointOrderingFilter.cs new file mode 100644 index 0000000000..2d9a1ba39f --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/EndpointOrderingFilter.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using JetBrains.Annotations; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + internal sealed class EndpointOrderingFilter : IDocumentFilter + { + private static readonly Regex RelationshipNameInUrlPattern = + new($".*{JsonApiRoutingTemplate.PrimaryEndpoint}/(?>{JsonApiRoutingTemplate.RelationshipsPart}\\/)?(\\w+)", RegexOptions.Compiled); + + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + ArgumentGuard.NotNull(swaggerDoc, nameof(swaggerDoc)); + ArgumentGuard.NotNull(context, nameof(context)); + + List> orderedEndpoints = swaggerDoc.Paths.OrderBy(GetPrimaryResourcePublicName) + .ThenBy(GetRelationshipName).ThenBy(IsSecondaryEndpoint).ToList(); + + swaggerDoc.Paths.Clear(); + + foreach ((string url, OpenApiPathItem path) in orderedEndpoints) + { + swaggerDoc.Paths.Add(url, path); + } + } + + private static string GetPrimaryResourcePublicName(KeyValuePair entry) + { + return entry.Value.Operations.First().Value.Tags.First().Name; + } + + private static bool IsSecondaryEndpoint(KeyValuePair entry) + { + return entry.Key.Contains("/" + JsonApiRoutingTemplate.RelationshipsPart); + } + + private static string GetRelationshipName(KeyValuePair entry) + { + Match match = RelationshipNameInUrlPattern.Match(entry.Key); + + return match.Success ? match.Groups[1].Value : string.Empty; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ISchemaRepositoryAccessor.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ISchemaRepositoryAccessor.cs new file mode 100644 index 0000000000..a3e40d79c4 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ISchemaRepositoryAccessor.cs @@ -0,0 +1,9 @@ +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + internal interface ISchemaRepositoryAccessor + { + SchemaRepository Current { get; } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs new file mode 100644 index 0000000000..97129f14ee --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; +using Newtonsoft.Json; +using Swashbuckle.AspNetCore.Newtonsoft; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + /// + /// For schema generation, we rely on from Swashbuckle for all but our own JSON:API types. + /// + internal sealed class JsonApiDataContractResolver : ISerializerDataContractResolver + { + private readonly NewtonsoftDataContractResolver _dataContractResolver; + private readonly IResourceContextProvider _resourceContextProvider; + + public JsonApiDataContractResolver(IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions) + { + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + + _resourceContextProvider = resourceContextProvider; + + JsonSerializerSettings serializerSettings = jsonApiOptions.SerializerSettings ?? new JsonSerializerSettings(); + _dataContractResolver = new NewtonsoftDataContractResolver(serializerSettings); + } + + public DataContract GetDataContractForType(Type type) + { + ArgumentGuard.NotNull(type, nameof(type)); + + if (type == typeof(IIdentifiable)) + { + // We have no way of telling Swashbuckle to opt out on this type, the closest we can get is return a contract with type Unknown. + return DataContract.ForDynamic(typeof(object)); + } + + DataContract dataContract = _dataContractResolver.GetDataContractForType(type); + + IList replacementProperties = null; + + if (type.IsSubclassOfOpenGeneric(typeof(Identifiable<>))) + { + replacementProperties = dataContract.ObjectProperties.Where(IsIdentity).ToArray(); + } + else if (type.IsAssignableTo(typeof(IIdentifiable))) + { + replacementProperties = GetDataPropertiesThatExistInResourceContext(type, dataContract); + } + + if (replacementProperties != null) + { + dataContract = ReplacePropertiesInDataContract(dataContract, replacementProperties); + } + + return dataContract; + } + + private static bool IsIdentity(DataProperty property) + { + return property.MemberInfo.Name == nameof(Identifiable.Id); + } + + private static DataContract ReplacePropertiesInDataContract(DataContract dataContract, IEnumerable dataProperties) + { + return DataContract.ForObject(dataContract.UnderlyingType, dataProperties, dataContract.ObjectExtensionDataType, + dataContract.ObjectTypeNameProperty, dataContract.ObjectTypeNameValue); + } + + private IList GetDataPropertiesThatExistInResourceContext(Type resourceType, DataContract dataContract) + { + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + var dataProperties = new List(); + + foreach (DataProperty property in dataContract.ObjectProperties) + { + if (property.MemberInfo.Name == nameof(Identifiable.Id)) + { + // Schemas of JsonApiDotNetCore resources will obtain an Id property through inheritance of a resource identifier type. + continue; + } + + ResourceFieldAttribute matchingField = resourceContext.Fields.SingleOrDefault(field => + IsPropertyCompatibleWithMember(field.Property, property.MemberInfo)); + + if (matchingField != null) + { + DataProperty matchingProperty = matchingField.PublicName != property.Name + ? ChangeDataPropertyName(property, matchingField.PublicName) + : property; + + dataProperties.Add(matchingProperty); + } + } + + return dataProperties; + } + + private static DataProperty ChangeDataPropertyName(DataProperty property, string name) + { + return new(name, property.MemberType, property.IsRequired, property.IsNullable, property.IsReadOnly, property.IsWriteOnly, property.MemberInfo); + } + + private static bool IsPropertyCompatibleWithMember(PropertyInfo property, MemberInfo member) + { + // In JsonApiDotNetCore the PropertyInfo for Id stored in AttrAttribute is that of the ReflectedType, whereas Newtonsoft uses the DeclaringType. + return property == member || property.DeclaringType?.GetProperty(property.Name) == member; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiObjectNullabilityProcessor.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiObjectNullabilityProcessor.cs new file mode 100644 index 0000000000..be6f4c9283 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiObjectNullabilityProcessor.cs @@ -0,0 +1,132 @@ +using Microsoft.OpenApi.Models; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + /// + /// Removes unwanted nullability of entries in schemas of JSON:API documents. + /// + /// + /// Initially these entries are marked nullable by Swashbuckle because nullable reference types are not enabled. This post-processing step can be removed + /// entirely once we enable nullable reference types. See eg + /// https://github.com/degreed/JsonApiCorePrototype/blob/936db8950d925f1b8a055cf5d8bba753f6579094/src/Web/OpenApi/JsonApiObjects/Documents/ManyResourceIdentifierResponseDocument.cs#L7 + /// + internal sealed class JsonApiObjectNullabilityProcessor + { + private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; + + public JsonApiObjectNullabilityProcessor(ISchemaRepositoryAccessor schemaRepositoryAccessor) + { + ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); + + _schemaRepositoryAccessor = schemaRepositoryAccessor; + } + + public void ClearDocumentProperties(OpenApiSchema referenceSchemaForDocument) + { + ArgumentGuard.NotNull(referenceSchemaForDocument, nameof(referenceSchemaForDocument)); + + OpenApiSchema fullSchemaForDocument = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForDocument.Reference.Id]; + + ClearMetaObjectNullability(fullSchemaForDocument); + ClearJsonapiObjectNullability(fullSchemaForDocument); + ClearLinksObjectNullability(fullSchemaForDocument); + + OpenApiSchema fullSchemaForResourceObject = TryGetFullSchemaForResourceObject(fullSchemaForDocument); + + if (fullSchemaForResourceObject != null) + { + ClearResourceObjectNullability(fullSchemaForResourceObject); + } + } + + private static void ClearMetaObjectNullability(OpenApiSchema fullSchema) + { + if (fullSchema.Properties.ContainsKey(JsonApiObjectPropertyName.MetaObject)) + { + fullSchema.Properties[JsonApiObjectPropertyName.MetaObject].Nullable = false; + } + } + + private void ClearJsonapiObjectNullability(OpenApiSchema fullSchema) + { + if (fullSchema.Properties.ContainsKey(JsonApiObjectPropertyName.JsonapiObject)) + { + OpenApiSchema fullSchemaForJsonapiObject = + _schemaRepositoryAccessor.Current.Schemas[fullSchema.Properties[JsonApiObjectPropertyName.JsonapiObject].Reference.Id]; + + fullSchemaForJsonapiObject.Properties[JsonApiObjectPropertyName.JsonapiObjectVersion].Nullable = false; + fullSchemaForJsonapiObject.Properties[JsonApiObjectPropertyName.JsonapiObjectExt].Nullable = false; + fullSchemaForJsonapiObject.Properties[JsonApiObjectPropertyName.JsonapiObjectProfile].Nullable = false; + ClearMetaObjectNullability(fullSchemaForJsonapiObject); + } + } + + private void ClearLinksObjectNullability(OpenApiSchema fullSchema) + { + if (fullSchema.Properties.ContainsKey(JsonApiObjectPropertyName.LinksObject)) + { + OpenApiSchema fullSchemaForLinksObject = + _schemaRepositoryAccessor.Current.Schemas[fullSchema.Properties[JsonApiObjectPropertyName.LinksObject].Reference.Id]; + + foreach (OpenApiSchema schemaForEntryInLinksObject in fullSchemaForLinksObject.Properties.Values) + { + schemaForEntryInLinksObject.Nullable = false; + } + } + } + + private OpenApiSchema TryGetFullSchemaForResourceObject(OpenApiSchema fullSchemaForDocument) + { + OpenApiSchema schemaForDataObject = fullSchemaForDocument.Properties[JsonApiObjectPropertyName.Data]; + OpenApiReference dataSchemaReference = schemaForDataObject.Type == "array" ? schemaForDataObject.Items.Reference : schemaForDataObject.Reference; + + if (dataSchemaReference == null) + { + return null; + } + + return _schemaRepositoryAccessor.Current.Schemas[dataSchemaReference.Id]; + } + + private void ClearResourceObjectNullability(OpenApiSchema fullSchemaForValueOfData) + { + ClearMetaObjectNullability(fullSchemaForValueOfData); + ClearLinksObjectNullability(fullSchemaForValueOfData); + ClearAttributesObjectNullability(fullSchemaForValueOfData); + ClearRelationshipsObjectNullability(fullSchemaForValueOfData); + } + + private void ClearAttributesObjectNullability(OpenApiSchema fullSchemaForResourceObject) + { + if (fullSchemaForResourceObject.Properties.ContainsKey(JsonApiObjectPropertyName.AttributesObject)) + { + OpenApiSchema fullSchemaForAttributesObject = _schemaRepositoryAccessor.Current.Schemas[ + fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.AttributesObject].Reference.Id]; + + fullSchemaForAttributesObject.Nullable = false; + } + } + + private void ClearRelationshipsObjectNullability(OpenApiSchema fullSchemaForResourceObject) + { + if (fullSchemaForResourceObject.Properties.ContainsKey(JsonApiObjectPropertyName.RelationshipsObject)) + { + OpenApiSchema fullSchemaForRelationshipsObject = _schemaRepositoryAccessor.Current.Schemas[ + fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.RelationshipsObject].Reference.Id]; + + fullSchemaForRelationshipsObject.Nullable = false; + ClearRelationshipsDataNullability(fullSchemaForRelationshipsObject); + } + } + + private void ClearRelationshipsDataNullability(OpenApiSchema fullSchemaForRelationshipsObject) + { + foreach (OpenApiSchema relationshipObjectData in fullSchemaForRelationshipsObject.Properties.Values) + { + OpenApiSchema fullSchemaForRelationshipsObjectData = _schemaRepositoryAccessor.Current.Schemas[relationshipObjectData.Reference.Id]; + ClearLinksObjectNullability(fullSchemaForRelationshipsObjectData); + ClearMetaObjectNullability(fullSchemaForRelationshipsObjectData); + } + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs new file mode 100644 index 0000000000..a27f7915c9 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -0,0 +1,159 @@ +using System; +using System.Linq; +using System.Reflection; +using JsonApiDotNetCore.OpenApi.JsonApiObjects; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; +using JsonApiDotNetCore.Configuration; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + internal sealed class JsonApiSchemaGenerator : ISchemaGenerator + { + private static readonly Type[] JsonApiResourceDocumentOpenTypes = + { + typeof(ResourceCollectionResponseDocument<>), + typeof(PrimaryResourceResponseDocument<>), + typeof(SecondaryResourceResponseDocument<>), + typeof(ResourcePostRequestDocument<>), + typeof(ResourcePatchRequestDocument<>) + }; + + private static readonly Type[] SingleNonPrimaryDataDocumentOpenTypes = + { + typeof(ToOneRelationshipRequestData<>), + typeof(ResourceIdentifierResponseDocument<>), + typeof(SecondaryResourceResponseDocument<>) + }; + + private static readonly Type[] JsonApiResourceIdentifierDocumentOpenTypes = + { + typeof(ResourceIdentifierCollectionResponseDocument<>), + typeof(ResourceIdentifierResponseDocument<>) + }; + + private readonly ISchemaGenerator _defaultSchemaGenerator; + private readonly ResourceObjectSchemaGenerator _resourceObjectSchemaGenerator; + private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator; + private readonly JsonApiObjectNullabilityProcessor _jsonApiObjectNullabilityProcessor; + private readonly SchemaRepositoryAccessor _schemaRepositoryAccessor = new(); + + public JsonApiSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions) + { + ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + + _defaultSchemaGenerator = defaultSchemaGenerator; + _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(_schemaRepositoryAccessor); + _jsonApiObjectNullabilityProcessor = new JsonApiObjectNullabilityProcessor(_schemaRepositoryAccessor); + + _resourceObjectSchemaGenerator = + new ResourceObjectSchemaGenerator(defaultSchemaGenerator, resourceContextProvider, jsonApiOptions, _schemaRepositoryAccessor); + } + + public OpenApiSchema GenerateSchema(Type type, SchemaRepository schemaRepository, MemberInfo memberInfo = null, ParameterInfo parameterInfo = null) + { + ArgumentGuard.NotNull(type, nameof(type)); + ArgumentGuard.NotNull(schemaRepository, nameof(schemaRepository)); + + _schemaRepositoryAccessor.Current = schemaRepository; + + if (schemaRepository.TryLookupByType(type, out OpenApiSchema jsonApiDocumentSchema)) + { + return jsonApiDocumentSchema; + } + + OpenApiSchema schema; + + if (IsJsonApiResourceDocument(type)) + { + schema = GenerateResourceJsonApiDocumentSchema(type); + } + else + { + schema = _defaultSchemaGenerator.GenerateSchema(type, schemaRepository, memberInfo, parameterInfo); + } + + if (IsSingleNonPrimaryDataDocument(type)) + { + SetDataObjectSchemaToNullable(schema); + } + + if (IsJsonApiDocument(type)) + { + RemoveNotApplicableNullability(schema); + } + + return schema; + } + + private static bool IsJsonApiResourceDocument(Type type) + { + return type.IsConstructedGenericType && JsonApiResourceDocumentOpenTypes.Contains(type.GetGenericTypeDefinition()); + } + + private static bool IsJsonApiDocument(Type type) + { + return IsJsonApiResourceDocument(type) || IsJsonApiResourceIdentifierDocument(type); + } + + private static bool IsJsonApiResourceIdentifierDocument(Type type) + { + return type.IsConstructedGenericType && JsonApiResourceIdentifierDocumentOpenTypes.Contains(type.GetGenericTypeDefinition()); + } + + private OpenApiSchema GenerateResourceJsonApiDocumentSchema(Type type) + { + Type resourceObjectType = type.BaseType!.GenericTypeArguments[0]; + + if (!_schemaRepositoryAccessor.Current.TryLookupByType(resourceObjectType, out OpenApiSchema referenceSchemaForResourceObject)) + { + referenceSchemaForResourceObject = _resourceObjectSchemaGenerator.GenerateSchema(resourceObjectType); + } + + OpenApiSchema referenceSchemaForDataObject = + IsSingleDataDocument(type) ? referenceSchemaForResourceObject : CreateArrayTypeDataSchema(referenceSchemaForResourceObject); + + OpenApiSchema referenceSchemaForDocument = _defaultSchemaGenerator.GenerateSchema(type, _schemaRepositoryAccessor.Current); + OpenApiSchema fullSchemaForDocument = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForDocument.Reference.Id]; + + fullSchemaForDocument.Properties[JsonApiObjectPropertyName.Data] = referenceSchemaForDataObject; + + return referenceSchemaForDocument; + } + + private static bool IsSingleDataDocument(Type type) + { + return type.BaseType?.IsConstructedGenericType == true && type.BaseType.GetGenericTypeDefinition() == typeof(SingleData<>); + } + + private static bool IsSingleNonPrimaryDataDocument(Type type) + { + return type.IsConstructedGenericType && SingleNonPrimaryDataDocumentOpenTypes.Contains(type.GetGenericTypeDefinition()); + } + + 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); + } + + private static OpenApiSchema CreateArrayTypeDataSchema(OpenApiSchema referenceSchemaForResourceObject) + { + return new() + { + Items = referenceSchemaForResourceObject, + Type = "array" + }; + } + + private void RemoveNotApplicableNullability(OpenApiSchema schema) + { + _jsonApiObjectNullabilityProcessor.ClearDocumentProperties(schema); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs new file mode 100644 index 0000000000..3449b2abfc --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaGenerator.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using Microsoft.OpenApi.Models; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + internal sealed class NullableReferenceSchemaGenerator + { + private static readonly NullableReferenceSchemaStrategy NullableReferenceStrategy = + Enum.Parse(NullableReferenceSchemaStrategy.Implicit.ToString()); + + private static OpenApiSchema _referenceSchemaForNullValue; + private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; + + public NullableReferenceSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor) + { + ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); + + _schemaRepositoryAccessor = schemaRepositoryAccessor; + } + + public OpenApiSchema GenerateSchema(OpenApiSchema referenceSchema) + { + ArgumentGuard.NotNull(referenceSchema, nameof(referenceSchema)); + + return new OpenApiSchema + { + OneOf = new List + { + referenceSchema, + GetNullableReferenceSchema() + } + }; + } + + private OpenApiSchema GetNullableReferenceSchema() + { + return NullableReferenceStrategy == NullableReferenceSchemaStrategy.Explicit + ? GetNullableReferenceSchemaUsingExplicitNullType() + : GetNullableReferenceSchemaUsingImplicitNullType(); + } + + // This approach is supported in OAS starting from v3.1. See https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-580103688 + private static OpenApiSchema GetNullableReferenceSchemaUsingExplicitNullType() + { + return new() + { + Type = "null" + }; + } + + // This approach is supported starting from OAS v3.0. See https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-487314681 + private OpenApiSchema GetNullableReferenceSchemaUsingImplicitNullType() + { + if (_referenceSchemaForNullValue != null) + { + return _referenceSchemaForNullValue; + } + + 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() + } + }; + + _referenceSchemaForNullValue = _schemaRepositoryAccessor.Current.AddDefinition("null-value", fullSchemaForNullValue); + + return _referenceSchemaForNullValue; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaStrategy.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaStrategy.cs new file mode 100644 index 0000000000..5580f776ee --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/NullableReferenceSchemaStrategy.cs @@ -0,0 +1,8 @@ +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 new file mode 100644 index 0000000000..ab08ae3948 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldObjectSchemaBuilder.cs @@ -0,0 +1,243 @@ +using System; +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; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + internal sealed class ResourceFieldObjectSchemaBuilder + { + private static readonly SchemaRepository ResourceSchemaRepository = new(); + + private readonly ResourceTypeInfo _resourceTypeInfo; + private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; + private readonly SchemaGenerator _defaultSchemaGenerator; + private readonly JsonApiSchemaIdSelector _jsonApiSchemaIdSelector; + private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator; + private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator; + private readonly IDictionary _schemasForResourceFields; + + public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISchemaRepositoryAccessor schemaRepositoryAccessor, + SchemaGenerator defaultSchemaGenerator, JsonApiSchemaIdSelector jsonApiSchemaIdSelector, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator) + { + ArgumentGuard.NotNull(resourceTypeInfo, nameof(resourceTypeInfo)); + ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); + ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); + ArgumentGuard.NotNull(jsonApiSchemaIdSelector, nameof(jsonApiSchemaIdSelector)); + ArgumentGuard.NotNull(resourceTypeSchemaGenerator, nameof(resourceTypeSchemaGenerator)); + + _resourceTypeInfo = resourceTypeInfo; + _schemaRepositoryAccessor = schemaRepositoryAccessor; + _defaultSchemaGenerator = defaultSchemaGenerator; + _jsonApiSchemaIdSelector = jsonApiSchemaIdSelector; + _resourceTypeSchemaGenerator = resourceTypeSchemaGenerator; + + _nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor); + _schemasForResourceFields = GetFieldSchemas(); + } + + private IDictionary GetFieldSchemas() + { + if (!ResourceSchemaRepository.TryLookupByType(_resourceTypeInfo.ResourceType, out OpenApiSchema referenceSchemaForResource)) + { + referenceSchemaForResource = _defaultSchemaGenerator.GenerateSchema(_resourceTypeInfo.ResourceType, ResourceSchemaRepository); + } + + OpenApiSchema fullSchemaForResource = ResourceSchemaRepository.Schemas[referenceSchemaForResource.Reference.Id]; + return fullSchemaForResource.Properties; + } + + public OpenApiSchema BuildAttributesObject(OpenApiSchema fullSchemaForResourceObject) + { + ArgumentGuard.NotNull(fullSchemaForResourceObject, nameof(fullSchemaForResourceObject)); + + OpenApiSchema fullSchemaForAttributesObject = fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.AttributesObject]; + + SetMembersOfAttributesObject(fullSchemaForAttributesObject); + + fullSchemaForAttributesObject.AdditionalPropertiesAllowed = false; + + if (fullSchemaForAttributesObject.Properties.Any()) + { + return GetReferenceSchemaForFieldObject(fullSchemaForAttributesObject, JsonApiObjectPropertyName.AttributesObject); + } + + return null; + } + + private void SetMembersOfAttributesObject(OpenApiSchema fullSchemaForAttributesObject) + { + AttrCapabilities requiredCapability = GetRequiredCapabilityForAttributes(_resourceTypeInfo.ResourceObjectOpenType); + + foreach ((string fieldName, OpenApiSchema resourceFieldSchema) in _schemasForResourceFields) + { + var matchingAttribute = _resourceTypeInfo.TryGetResourceFieldByName(fieldName); + + if (matchingAttribute != null && matchingAttribute.Capabilities.HasFlag(requiredCapability)) + { + AddAttributeSchemaToResourceObject(matchingAttribute, fullSchemaForAttributesObject, resourceFieldSchema); + + if (IsAttributeRequired(_resourceTypeInfo.ResourceObjectOpenType, matchingAttribute)) + { + fullSchemaForAttributesObject.Required.Add(matchingAttribute.PublicName); + } + } + } + } + + private static AttrCapabilities GetRequiredCapabilityForAttributes(Type resourceObjectOpenType) + { + return resourceObjectOpenType == typeof(ResourceResponseObject<>) ? AttrCapabilities.AllowView : + resourceObjectOpenType == typeof(ResourcePostRequestObject<>) ? AttrCapabilities.AllowCreate : + resourceObjectOpenType == typeof(ResourcePatchRequestObject<>) ? AttrCapabilities.AllowChange : throw new UnreachableCodeException(); + } + + private void AddAttributeSchemaToResourceObject(AttrAttribute attribute, OpenApiSchema attributesObjectSchema, OpenApiSchema resourceAttributeSchema) + { + if (resourceAttributeSchema.Reference != null && !_schemaRepositoryAccessor.Current.TryLookupByType(attribute.Property.PropertyType, out _)) + { + ExposeSchema(resourceAttributeSchema.Reference, attribute.Property.PropertyType); + } + + attributesObjectSchema.Properties.Add(attribute.PublicName, resourceAttributeSchema); + } + + 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 static bool IsAttributeRequired(Type resourceObjectOpenType, AttrAttribute matchingAttribute) + { + return resourceObjectOpenType == typeof(ResourcePostRequestObject<>) && matchingAttribute.Property.GetCustomAttribute() != null; + } + + 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. + string resourceObjectSchemaId = _jsonApiSchemaIdSelector.GetSchemaId(_resourceTypeInfo.ResourceObjectType); + string fieldObjectSchemaId = resourceObjectSchemaId.Replace(JsonApiObjectPropertyName.Data, fieldObjectName); + + return _schemaRepositoryAccessor.Current.AddDefinition(fieldObjectSchemaId, fullSchema); + } + + public OpenApiSchema BuildRelationshipsObject(OpenApiSchema fullSchemaForResourceObject) + { + ArgumentGuard.NotNull(fullSchemaForResourceObject, nameof(fullSchemaForResourceObject)); + + OpenApiSchema fullSchemaForRelationshipsObject = fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.RelationshipsObject]; + + SetMembersOfRelationshipsObject(fullSchemaForRelationshipsObject); + + fullSchemaForRelationshipsObject.AdditionalPropertiesAllowed = false; + + if (fullSchemaForRelationshipsObject.Properties.Any()) + { + return GetReferenceSchemaForFieldObject(fullSchemaForRelationshipsObject, JsonApiObjectPropertyName.RelationshipsObject); + } + + return null; + } + + private void SetMembersOfRelationshipsObject(OpenApiSchema fullSchemaForRelationshipsObject) + { + foreach (string fieldName in _schemasForResourceFields.Keys) + { + var matchingRelationship = _resourceTypeInfo.TryGetResourceFieldByName(fieldName); + + if (matchingRelationship != null) + { + EnsureResourceIdentifierObjectSchemaExists(matchingRelationship); + AddRelationshipDataSchemaToResourceObject(matchingRelationship, fullSchemaForRelationshipsObject); + } + } + } + + private void EnsureResourceIdentifierObjectSchemaExists(RelationshipAttribute relationship) + { + Type resourceIdentifierObjectType = typeof(ResourceIdentifierObject<>).MakeGenericType(relationship.RightType); + + if (!ResourceIdentifierObjectSchemaExists(resourceIdentifierObjectType)) + { + GenerateResourceIdentifierObjectSchema(resourceIdentifierObjectType); + } + } + + private bool ResourceIdentifierObjectSchemaExists(Type resourceIdentifierObjectType) + { + return _schemaRepositoryAccessor.Current.TryLookupByType(resourceIdentifierObjectType, out _); + } + + private void GenerateResourceIdentifierObjectSchema(Type resourceIdentifierObjectType) + { + OpenApiSchema referenceSchemaForResourceIdentifierObject = + _defaultSchemaGenerator.GenerateSchema(resourceIdentifierObjectType, _schemaRepositoryAccessor.Current); + + OpenApiSchema fullSchemaForResourceIdentifierObject = + _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForResourceIdentifierObject.Reference.Id]; + + Type resourceType = resourceIdentifierObjectType.GetGenericArguments()[0]; + fullSchemaForResourceIdentifierObject.Properties[JsonApiObjectPropertyName.Type] = _resourceTypeSchemaGenerator.Get(resourceType); + } + + private void AddRelationshipDataSchemaToResourceObject(RelationshipAttribute relationship, OpenApiSchema relationshipObjectSchema) + { + Type relationshipDataType = GetRelationshipDataType(relationship, _resourceTypeInfo.ResourceObjectOpenType); + + OpenApiSchema referenceSchemaForRelationshipData = TryGetReferenceSchemaForRelationshipData(relationshipDataType) ?? + CreateRelationshipDataObjectSchema(relationship, relationshipDataType); + + relationshipObjectSchema.Properties.Add(relationship.PublicName, referenceSchemaForRelationshipData); + } + + private static Type GetRelationshipDataType(RelationshipAttribute relationship, Type resourceObjectType) + { + if (resourceObjectType.GetGenericTypeDefinition().IsAssignableTo(typeof(ResourceResponseObject<>))) + { + return relationship is HasOneAttribute + ? typeof(ToOneRelationshipResponseData<>).MakeGenericType(relationship.RightType) + : typeof(ToManyRelationshipResponseData<>).MakeGenericType(relationship.RightType); + } + + return relationship is HasOneAttribute + ? typeof(ToOneRelationshipRequestData<>).MakeGenericType(relationship.RightType) + : typeof(ToManyRelationshipRequestData<>).MakeGenericType(relationship.RightType); + } + + private OpenApiSchema TryGetReferenceSchemaForRelationshipData(Type relationshipDataType) + { + _schemaRepositoryAccessor.Current.TryLookupByType(relationshipDataType, out OpenApiSchema referenceSchemaForRelationshipData); + return referenceSchemaForRelationshipData; + } + + private OpenApiSchema CreateRelationshipDataObjectSchema(RelationshipAttribute relationship, Type relationshipDataType) + { + OpenApiSchema referenceSchema = _defaultSchemaGenerator.GenerateSchema(relationshipDataType, _schemaRepositoryAccessor.Current); + + OpenApiSchema fullSchema = _schemaRepositoryAccessor.Current.Schemas[referenceSchema.Reference.Id]; + + Type relationshipDataOpenType = relationshipDataType.GetGenericTypeDefinition(); + + if (relationshipDataOpenType == typeof(ToOneRelationshipResponseData<>) || relationshipDataOpenType == typeof(ToManyRelationshipResponseData<>)) + { + fullSchema.Required.Remove(JsonApiObjectPropertyName.Data); + } + + if (relationship is HasOneAttribute) + { + fullSchema.Properties[JsonApiObjectPropertyName.Data] = + _nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiObjectPropertyName.Data]); + } + + return referenceSchema; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs new file mode 100644 index 0000000000..f0f65fd21f --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; +using JsonApiDotNetCore.Configuration; +using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Serialization; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + internal sealed class ResourceObjectSchemaGenerator + { + private readonly SchemaGenerator _defaultSchemaGenerator; + private readonly IResourceContextProvider _resourceContextProvider; + private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; + private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator; + private readonly bool _allowClientGeneratedIds; + private readonly Func _createFieldObjectBuilderFactory; + + public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IResourceContextProvider resourceContextProvider, + IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor) + { + ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + ArgumentGuard.NotNull(jsonApiOptions, nameof(jsonApiOptions)); + ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); + + _defaultSchemaGenerator = defaultSchemaGenerator; + _resourceContextProvider = resourceContextProvider; + _schemaRepositoryAccessor = schemaRepositoryAccessor; + + _resourceTypeSchemaGenerator = new ResourceTypeSchemaGenerator(schemaRepositoryAccessor, resourceContextProvider); + _allowClientGeneratedIds = jsonApiOptions.AllowClientGeneratedIds; + + _createFieldObjectBuilderFactory = CreateFieldObjectBuilderFactory(defaultSchemaGenerator, resourceContextProvider, jsonApiOptions, + schemaRepositoryAccessor, _resourceTypeSchemaGenerator); + } + + private static Func CreateFieldObjectBuilderFactory(SchemaGenerator defaultSchemaGenerator, + IResourceContextProvider resourceContextProvider, IJsonApiOptions jsonApiOptions, ISchemaRepositoryAccessor schemaRepositoryAccessor, + ResourceTypeSchemaGenerator resourceTypeSchemaGenerator) + { + NamingStrategy namingStrategy = ((DefaultContractResolver)jsonApiOptions.SerializerSettings.ContractResolver)!.NamingStrategy; + ResourceNameFormatterProxy resourceNameFormatterProxy = new(namingStrategy); + var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatterProxy, resourceContextProvider); + + return resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor, defaultSchemaGenerator, + jsonApiSchemaIdSelector, resourceTypeSchemaGenerator); + } + + public OpenApiSchema GenerateSchema(Type resourceObjectType) + { + ArgumentGuard.NotNull(resourceObjectType, nameof(resourceObjectType)); + + (OpenApiSchema fullSchemaForResourceObject, OpenApiSchema referenceSchemaForResourceObject) = EnsureSchemasExist(resourceObjectType); + + var resourceTypeInfo = ResourceTypeInfo.Create(resourceObjectType, _resourceContextProvider); + ResourceFieldObjectSchemaBuilder fieldObjectBuilder = _createFieldObjectBuilderFactory(resourceTypeInfo); + + RemoveResourceIdIfPostResourceObject(resourceTypeInfo.ResourceObjectOpenType, fullSchemaForResourceObject); + + SetResourceType(fullSchemaForResourceObject, resourceTypeInfo.ResourceType); + + SetResourceAttributes(fullSchemaForResourceObject, fieldObjectBuilder); + + SetResourceRelationships(fullSchemaForResourceObject, fieldObjectBuilder); + + ReorderMembers(fullSchemaForResourceObject, new[] + { + JsonApiObjectPropertyName.Type, + JsonApiObjectPropertyName.Id, + JsonApiObjectPropertyName.AttributesObject, + JsonApiObjectPropertyName.RelationshipsObject, + JsonApiObjectPropertyName.LinksObject, + JsonApiObjectPropertyName.MetaObject + }); + + return referenceSchemaForResourceObject; + } + + private (OpenApiSchema fullSchema, OpenApiSchema referenceSchema) EnsureSchemasExist(Type resourceObjectType) + { + if (!_schemaRepositoryAccessor.Current.TryLookupByType(resourceObjectType, out OpenApiSchema referenceSchema)) + { + referenceSchema = _defaultSchemaGenerator.GenerateSchema(resourceObjectType, _schemaRepositoryAccessor.Current); + } + + OpenApiSchema fullSchema = _schemaRepositoryAccessor.Current.Schemas[referenceSchema.Reference.Id]; + + return (fullSchema, referenceSchema); + } + + private void RemoveResourceIdIfPostResourceObject(Type resourceObjectOpenType, OpenApiSchema fullSchemaForResourceObject) + { + if (resourceObjectOpenType == typeof(ResourcePostRequestObject<>) && !_allowClientGeneratedIds) + { + fullSchemaForResourceObject.Required.Remove(JsonApiObjectPropertyName.Id); + fullSchemaForResourceObject.Properties.Remove(JsonApiObjectPropertyName.Id); + } + } + + private void SetResourceType(OpenApiSchema fullSchemaForResourceObject, Type resourceType) + { + fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.Type] = _resourceTypeSchemaGenerator.Get(resourceType); + } + + private static void SetResourceAttributes(OpenApiSchema fullSchemaForResourceObject, ResourceFieldObjectSchemaBuilder builder) + { + OpenApiSchema fullSchemaForAttributesObject = builder.BuildAttributesObject(fullSchemaForResourceObject); + + if (fullSchemaForAttributesObject != null) + { + fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.AttributesObject] = fullSchemaForAttributesObject; + } + else + { + fullSchemaForResourceObject.Properties.Remove(JsonApiObjectPropertyName.AttributesObject); + } + } + + private static void SetResourceRelationships(OpenApiSchema fullSchemaForResourceObject, ResourceFieldObjectSchemaBuilder builder) + { + OpenApiSchema fullSchemaForRelationshipsObject = builder.BuildRelationshipsObject(fullSchemaForResourceObject); + + if (fullSchemaForRelationshipsObject != null) + { + fullSchemaForResourceObject.Properties[JsonApiObjectPropertyName.RelationshipsObject] = fullSchemaForRelationshipsObject; + } + else + { + fullSchemaForResourceObject.Properties.Remove(JsonApiObjectPropertyName.RelationshipsObject); + } + } + + private static void ReorderMembers(OpenApiSchema fullSchemaForResourceObject, IEnumerable orderedMembers) + { + var reorderedMembers = new Dictionary(); + + foreach (string member in orderedMembers) + { + if (fullSchemaForResourceObject.Properties.ContainsKey(member)) + { + reorderedMembers[member] = fullSchemaForResourceObject.Properties[member]; + } + } + + fullSchemaForResourceObject.Properties = reorderedMembers; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs new file mode 100644 index 0000000000..5dc13b24b8 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeInfo.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources.Annotations; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + internal sealed class ResourceTypeInfo + { + private readonly ResourceContext _resourceContext; + + public Type ResourceObjectType { get; } + public Type ResourceObjectOpenType { get; } + public Type ResourceType { get; } + + private ResourceTypeInfo(Type resourceObjectType, Type resourceObjectOpenType, Type resourceType, ResourceContext resourceContext) + { + _resourceContext = resourceContext; + + ResourceObjectType = resourceObjectType; + ResourceObjectOpenType = resourceObjectOpenType; + ResourceType = resourceType; + } + + public static ResourceTypeInfo Create(Type resourceObjectType, IResourceContextProvider resourceContextProvider) + { + ArgumentGuard.NotNull(resourceObjectType, nameof(resourceObjectType)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + Type resourceObjectOpenType = resourceObjectType.GetGenericTypeDefinition(); + Type resourceType = resourceObjectType.GenericTypeArguments[0]; + ResourceContext resourceContext = resourceContextProvider.GetResourceContext(resourceType); + + return new ResourceTypeInfo(resourceObjectType, resourceObjectOpenType, resourceType, resourceContext); + } + + public TResourceFieldAttribute TryGetResourceFieldByName(string publicName) + where TResourceFieldAttribute : ResourceFieldAttribute + { + ArgumentGuard.NotNullNorEmpty(publicName, nameof(publicName)); + + return (TResourceFieldAttribute)_resourceContext.Fields.FirstOrDefault(field => field is TResourceFieldAttribute && field.PublicName == publicName); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs new file mode 100644 index 0000000000..2ceba85e18 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceTypeSchemaGenerator.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using JsonApiDotNetCore.Configuration; +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + internal sealed class ResourceTypeSchemaGenerator + { + private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor; + private readonly IResourceContextProvider _resourceContextProvider; + private readonly Dictionary _resourceTypeSchemaCache = new(); + + public ResourceTypeSchemaGenerator(ISchemaRepositoryAccessor schemaRepositoryAccessor, IResourceContextProvider resourceContextProvider) + { + ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor)); + ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); + + _schemaRepositoryAccessor = schemaRepositoryAccessor; + _resourceContextProvider = resourceContextProvider; + } + + public OpenApiSchema Get(Type resourceType) + { + ArgumentGuard.NotNull(resourceType, nameof(resourceType)); + + if (_resourceTypeSchemaCache.TryGetValue(resourceType, out OpenApiSchema referenceSchema)) + { + return referenceSchema; + } + + ResourceContext resourceContext = _resourceContextProvider.GetResourceContext(resourceType); + + var fullSchema = new OpenApiSchema + { + Type = "string", + Enum = new List + { + new OpenApiString(resourceContext.PublicName) + } + }; + + referenceSchema = new OpenApiSchema + { + Reference = new OpenApiReference + { + Id = $"{resourceContext.PublicName}-resource-type", + Type = ReferenceType.Schema + } + }; + + _schemaRepositoryAccessor.Current.AddDefinition(referenceSchema.Reference.Id, fullSchema); + _resourceTypeSchemaCache.Add(resourceContext.ResourceType, referenceSchema); + + return referenceSchema; + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/SchemaRepositoryAccessor.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/SchemaRepositoryAccessor.cs new file mode 100644 index 0000000000..e0a909f68a --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/SchemaRepositoryAccessor.cs @@ -0,0 +1,29 @@ +using System; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace JsonApiDotNetCore.OpenApi.SwaggerComponents +{ + internal sealed class SchemaRepositoryAccessor : ISchemaRepositoryAccessor + { + private SchemaRepository _schemaRepository; + + public SchemaRepository Current + { + get + { + if (_schemaRepository == null) + { + throw new InvalidOperationException("SchemaRepository unavailable."); + } + + return _schemaRepository; + } + set + { + ArgumentGuard.NotNull(value, nameof(Current)); + + _schemaRepository = value; + } + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/UnreachableCodeException.cs b/src/JsonApiDotNetCore.OpenApi/UnreachableCodeException.cs new file mode 100644 index 0000000000..bdb05a7e86 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/UnreachableCodeException.cs @@ -0,0 +1,12 @@ +using System; + +namespace JsonApiDotNetCore.OpenApi +{ + internal sealed class UnreachableCodeException : Exception + { + public UnreachableCodeException() + : base("This code should not be reachable.") + { + } + } +} diff --git a/src/JsonApiDotNetCore/TypeExtensions.cs b/src/JsonApiDotNetCore/TypeExtensions.cs index d25599b821..df41d66840 100644 --- a/src/JsonApiDotNetCore/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/TypeExtensions.cs @@ -19,5 +19,34 @@ public static bool IsOrImplementsInterface(this Type source, Type interfaceType) return source == interfaceType || source.GetInterfaces().Any(type => type == interfaceType); } + + /// + /// Whether the specified source type inherits from the specified open generic type. + /// + internal static bool IsSubclassOfOpenGeneric(this Type source, Type openGenericType) + { + ArgumentGuard.NotNull(openGenericType, nameof(openGenericType)); + ArgumentGuard.NotNull(source, nameof(openGenericType)); + + // TODO: check if source should be allowed null and return false in that case? + + Type typeInInheritanceTreeOfSource = source; + + while (typeInInheritanceTreeOfSource != null && typeInInheritanceTreeOfSource != typeof(object)) + { + Type typeToCheck = typeInInheritanceTreeOfSource.IsGenericType + ? typeInInheritanceTreeOfSource.GetGenericTypeDefinition() + : typeInInheritanceTreeOfSource; + + if (openGenericType == typeToCheck) + { + return true; + } + + typeInInheritanceTreeOfSource = typeInInheritanceTreeOfSource.BaseType; + } + + return false; + } } } From 54a023f806682a35671a7f525d23e3703aa37c52 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 8 Sep 2021 19:44:54 +0200 Subject: [PATCH 02/35] Add models and adjusted models in OpenAPI document tests such that i reflect features that were covered in the private repository --- ...onApiActionDescriptorCollectionProvider.cs | 1 - .../ServiceCollectionExtensions.cs | 1 - test/OpenApiTests/AircraftType.cs | 10 + test/OpenApiTests/Airplane.cs | 17 +- test/OpenApiTests/AirplanesController.cs | 4 +- test/OpenApiTests/Flight.cs | 14 +- test/OpenApiTests/FlightAttendant.cs | 42 + test/OpenApiTests/FlightAttendantExpertise.cs | 10 + test/OpenApiTests/OpenApiDbContext.cs | 19 + test/OpenApiTests/OpenApiStartup.cs | 20 + test/OpenApiTests/swagger.json | 1714 +++++++++++++---- 11 files changed, 1459 insertions(+), 393 deletions(-) create mode 100644 test/OpenApiTests/AircraftType.cs create mode 100644 test/OpenApiTests/FlightAttendant.cs create mode 100644 test/OpenApiTests/FlightAttendantExpertise.cs diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs index b704c698a1..ec90f37d6e 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs @@ -100,7 +100,6 @@ private static IList AddJsonApiMetadataToAction(ActionDescript private static void UpdateProducesResponseTypeAttribute(ActionDescriptor endpoint, Type responseTypeToSet) { - // TODO: is this check really adding anything? is the "else" ever hit, and if so, is that even meaningful? if (ProducesJsonApiResponseBody(endpoint)) { var producesResponse = endpoint.GetFilterMetadata(); diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index 562e15fb58..8753d3747d 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.Extensions.DependencyInjection; -using Microsoft.OpenApi.Models; using Newtonsoft.Json.Serialization; using Swashbuckle.AspNetCore.Swagger; using Swashbuckle.AspNetCore.SwaggerGen; diff --git a/test/OpenApiTests/AircraftType.cs b/test/OpenApiTests/AircraftType.cs new file mode 100644 index 0000000000..bd132d5acc --- /dev/null +++ b/test/OpenApiTests/AircraftType.cs @@ -0,0 +1,10 @@ +namespace OpenApiTests +{ + public enum AircraftType + { + Turboprops, + LightJet, + MidSizeJet, + JumboJet + } +} diff --git a/test/OpenApiTests/Airplane.cs b/test/OpenApiTests/Airplane.cs index 33a94b5013..35e1f93454 100644 --- a/test/OpenApiTests/Airplane.cs +++ b/test/OpenApiTests/Airplane.cs @@ -7,13 +7,22 @@ namespace OpenApiTests { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Airplane : Identifiable + public sealed class Airplane : Identifiable { - [Attr] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] public int SeatingCapacity { get; set; } - [Attr] - public DateTimeOffset ManufacturedAt { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + public long SerialNumber { get; set; } + + [Attr(PublicName = "airplane-type", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + public AircraftType AircraftType { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + public DateTime ManufacturedAt { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + public DateTime? LastServicedAt { get; set; } [HasMany] public ISet Flights { get; set; } diff --git a/test/OpenApiTests/AirplanesController.cs b/test/OpenApiTests/AirplanesController.cs index 002f4b7b1e..104f39a1eb 100644 --- a/test/OpenApiTests/AirplanesController.cs +++ b/test/OpenApiTests/AirplanesController.cs @@ -5,9 +5,9 @@ namespace OpenApiTests { - public sealed class AirplanesController : JsonApiController + public sealed class AirplanesController : JsonApiController { - public AirplanesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + public AirplanesController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/OpenApiTests/Flight.cs b/test/OpenApiTests/Flight.cs index 9ec0443b8e..bddd4c1d99 100644 --- a/test/OpenApiTests/Flight.cs +++ b/test/OpenApiTests/Flight.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -8,10 +9,19 @@ namespace OpenApiTests [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Flight : Identifiable { - [Attr] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] public string Destination { get; set; } - [Attr] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] public DateTimeOffset DepartsAt { get; set; } + + [HasOne] + public Airplane ServicingAirplane { get; set; } + + [HasMany(PublicName = "flight-attendants")] + public ISet CabinPersonnel { get; set; } + + [HasOne] + public FlightAttendant Purser { get; set; } } } diff --git a/test/OpenApiTests/FlightAttendant.cs b/test/OpenApiTests/FlightAttendant.cs new file mode 100644 index 0000000000..5b04b35450 --- /dev/null +++ b/test/OpenApiTests/FlightAttendant.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class FlightAttendant : Identifiable + { + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowFilter)] + public override long Id { get; set; } + + [Attr(Capabilities = AttrCapabilities.None)] + public FlightAttendantExpertise ExpertiseLevel { get; set; } + + [Attr(PublicName = "document-number", Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] + [Required] + [MaxLength(9)] + public string PassportNumber { get; set; } + + [Attr(Capabilities = AttrCapabilities.All)] + [Required] + [EmailAddress] + public string EmailAddress { get; set; } + + [Attr(Capabilities = AttrCapabilities.All)] + [Required] + [Url] + public string ProfileImageUrl { get; set; } + + [Attr(Capabilities = AttrCapabilities.All)] + public ICollection DestinationPreferences { get; set; } + + [HasMany] + public ISet Flights { get; set; } + + [HasOne] + public Flight PurserOnFlight { get; set; } + } +} diff --git a/test/OpenApiTests/FlightAttendantExpertise.cs b/test/OpenApiTests/FlightAttendantExpertise.cs new file mode 100644 index 0000000000..f13544d23b --- /dev/null +++ b/test/OpenApiTests/FlightAttendantExpertise.cs @@ -0,0 +1,10 @@ +namespace OpenApiTests +{ + public enum FlightAttendantExpertise + { + Junior, + Intermediate, + Senior, + Purser + } +} diff --git a/test/OpenApiTests/OpenApiDbContext.cs b/test/OpenApiTests/OpenApiDbContext.cs index 77e99aaddc..ea3571b3ff 100644 --- a/test/OpenApiTests/OpenApiDbContext.cs +++ b/test/OpenApiTests/OpenApiDbContext.cs @@ -1,6 +1,9 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +// @formatter:wrap_chained_method_calls chop_always +// @formatter:keep_existing_linebreaks true + namespace OpenApiTests { [UsedImplicitly(ImplicitUseTargetFlags.Members)] @@ -8,10 +11,26 @@ public sealed class OpenApiDbContext : DbContext { public DbSet Airplanes { get; set; } public DbSet Flights { get; set; } + public DbSet FlightAttendants { get; set; } public OpenApiDbContext(DbContextOptions options) : base(options) { } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.Entity() + .HasMany(flight => flight.CabinPersonnel) + .WithMany(flightAttendant => flightAttendant.Flights); + + builder.Entity() + .Ignore(flightAttendant => flightAttendant.DestinationPreferences); + + builder.Entity() + .HasOne(flight => flight.Purser) + .WithOne(flightAttendant => flightAttendant.PurserOnFlight) + .HasForeignKey("PurserId"); + } } } diff --git a/test/OpenApiTests/OpenApiStartup.cs b/test/OpenApiTests/OpenApiStartup.cs index 88f956a0f6..b4ce081d3f 100644 --- a/test/OpenApiTests/OpenApiStartup.cs +++ b/test/OpenApiTests/OpenApiStartup.cs @@ -1,10 +1,12 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.OpenApi; +using JsonApiDotNetCore.Resources.Annotations; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Serialization; using TestBuildingBlocks; namespace OpenApiTests @@ -21,6 +23,24 @@ public override void ConfigureServices(IServiceCollection services) services.AddOpenApi(mvcBuilder); } + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.Namespace = "api/v1"; + options.DefaultPageSize = new PageSize(10); + options.MaximumPageSize = new PageSize(100); + options.MaximumPageNumber = new PageNumber(50); + options.IncludeTotalResourceCount = true; + options.ValidateModelState = true; + options.DefaultAttrCapabilities = AttrCapabilities.AllowView; + + options.SerializerSettings.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy() + }; + } + public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) { app.UseRouting(); diff --git a/test/OpenApiTests/swagger.json b/test/OpenApiTests/swagger.json index e90093c24d..16a1e222b8 100644 --- a/test/OpenApiTests/swagger.json +++ b/test/OpenApiTests/swagger.json @@ -5,181 +5,204 @@ "version": "1.0" }, "paths": { - "/airplanes": { + "/api/v1/airplanes": { "get": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "get-airplane-collection", "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/airplane-collection-response-document" + } + } + } } } }, "head": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "head-airplane-collection", "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/airplane-collection-response-document" + } + } + } } } }, "post": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "post-airplane", "requestBody": { "content": { - "application/json": { + "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/Airplane" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/Airplane" - } - }, - "application/*+json": { - "schema": { - "$ref": "#/components/schemas/Airplane" + "$ref": "#/components/schemas/airplane-post-request-document" } } } }, "responses": { - "200": { + "201": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/airplane-primary-response-document" + } + } + } + }, + "204": { "description": "Success" } } } }, - "/airplanes/{id}": { + "/api/v1/airplanes/{id}": { "get": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "get-airplane", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/airplane-primary-response-document" + } + } + } } } }, "head": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "head-airplane", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/airplane-primary-response-document" + } + } + } } } }, "patch": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "patch-airplane", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], "requestBody": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Airplane" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/Airplane" - } - }, - "application/*+json": { + "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/Airplane" + "$ref": "#/components/schemas/airplane-patch-request-document" } } } }, "responses": { "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/airplane-primary-response-document" + } + } + } + }, + "204": { "description": "Success" } } }, "delete": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "delete-airplane", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], "responses": { - "200": { + "204": { "description": "Success" } } } }, - "/airplanes/{id}/{relationshipName}": { + "/api/v1/airplanes/{id}/flights": { "get": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "get-airplane-flights", "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, "schema": { "type": "string" } @@ -187,28 +210,27 @@ ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-collection-response-document" + } + } + } } } }, "head": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "head-airplane-flights", "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, "schema": { "type": "string" } @@ -216,30 +238,29 @@ ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-collection-response-document" + } + } + } } } } }, - "/airplanes/{id}/relationships/{relationshipName}": { + "/api/v1/airplanes/{id}/relationships/flights": { "get": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "get-airplane-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, "schema": { "type": "string" } @@ -247,28 +268,27 @@ ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-identifier-collection-response-document" + } + } + } } } }, "head": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "head-airplane-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, "schema": { "type": "string" } @@ -276,28 +296,27 @@ ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-identifier-collection-response-document" + } + } + } } } }, "post": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "post-airplane-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, "schema": { "type": "string" } @@ -305,59 +324,29 @@ ], "requestBody": { "content": { - "application/json": { - "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } - } - }, - "text/json": { - "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } - } - }, - "application/*+json": { + "application/vnd.api+json": { "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } + "$ref": "#/components/schemas/to-many-flight-request-data" } } } }, "responses": { - "200": { + "204": { "description": "Success" } } }, "patch": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "patch-airplane-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, "schema": { "type": "string" } @@ -365,41 +354,29 @@ ], "requestBody": { "content": { - "application/json": { - "schema": {} - }, - "text/json": { - "schema": {} - }, - "application/*+json": { - "schema": {} + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } } } }, "responses": { - "200": { + "204": { "description": "Success" } } }, "delete": { "tags": [ - "Airplanes" + "airplanes" ], + "operationId": "delete-airplane-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, - "schema": { - "type": "integer", - "format": "int32" - } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, "schema": { "type": "string" } @@ -407,98 +384,94 @@ ], "requestBody": { "content": { - "application/json": { - "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } - } - }, - "text/json": { - "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } - } - }, - "application/*+json": { + "application/vnd.api+json": { "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } + "$ref": "#/components/schemas/to-many-flight-request-data" } } } }, "responses": { - "200": { + "204": { "description": "Success" } } } }, - "/flights": { + "/api/v1/flights": { "get": { "tags": [ - "Flights" + "flights" ], + "operationId": "get-flight-collection", "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-collection-response-document" + } + } + } } } }, "head": { "tags": [ - "Flights" + "flights" ], + "operationId": "head-flight-collection", "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-collection-response-document" + } + } + } } } }, "post": { "tags": [ - "Flights" + "flights" ], + "operationId": "post-flight", "requestBody": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Flight" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/Flight" - } - }, - "application/*+json": { + "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/Flight" + "$ref": "#/components/schemas/flight-post-request-document" } } } }, "responses": { - "200": { + "201": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-primary-response-document" + } + } + } + }, + "204": { "description": "Success" } } } }, - "/flights/{id}": { + "/api/v1/flights/{id}": { "get": { "tags": [ - "Flights" + "flights" ], + "operationId": "get-flight", "parameters": [ { "name": "id", @@ -512,14 +485,22 @@ ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-primary-response-document" + } + } + } } } }, "head": { "tags": [ - "Flights" + "flights" ], + "operationId": "head-flight", "parameters": [ { "name": "id", @@ -533,14 +514,22 @@ ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-primary-response-document" + } + } + } } } }, "patch": { "tags": [ - "Flights" + "flights" ], + "operationId": "patch-flight", "parameters": [ { "name": "id", @@ -554,33 +543,34 @@ ], "requestBody": { "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Flight" - } - }, - "text/json": { - "schema": { - "$ref": "#/components/schemas/Flight" - } - }, - "application/*+json": { + "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/Flight" + "$ref": "#/components/schemas/flight-patch-request-document" } } } }, "responses": { "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-primary-response-document" + } + } + } + }, + "204": { "description": "Success" } } }, "delete": { "tags": [ - "Flights" + "flights" ], + "operationId": "delete-flight", "parameters": [ { "name": "id", @@ -593,17 +583,18 @@ } ], "responses": { - "200": { + "204": { "description": "Success" } } } }, - "/flights/{id}/{relationshipName}": { + "/api/v1/flights/{id}/flight-attendants": { "get": { "tags": [ - "Flights" + "flights" ], + "operationId": "get-flight-flight-attendants", "parameters": [ { "name": "id", @@ -613,26 +604,26 @@ "type": "integer", "format": "int32" } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-collection-response-document" + } + } + } } } }, "head": { "tags": [ - "Flights" + "flights" ], + "operationId": "head-flight-flight-attendants", "parameters": [ { "name": "id", @@ -642,28 +633,28 @@ "type": "integer", "format": "int32" } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-collection-response-document" + } + } + } } } } }, - "/flights/{id}/relationships/{relationshipName}": { + "/api/v1/flights/{id}/relationships/flight-attendants": { "get": { "tags": [ - "Flights" + "flights" ], + "operationId": "get-flight-flight-attendants-relationship", "parameters": [ { "name": "id", @@ -673,26 +664,26 @@ "type": "integer", "format": "int32" } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" + } + } + } } } }, "head": { "tags": [ - "Flights" + "flights" ], + "operationId": "head-flight-flight-attendants-relationship", "parameters": [ { "name": "id", @@ -702,26 +693,26 @@ "type": "integer", "format": "int32" } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "responses": { "200": { - "description": "Success" + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" + } + } + } } } }, "post": { "tags": [ - "Flights" + "flights" ], + "operationId": "post-flight-flight-attendants-relationship", "parameters": [ { "name": "id", @@ -731,57 +722,28 @@ "type": "integer", "format": "int32" } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { "content": { - "application/json": { - "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } - } - }, - "text/json": { + "application/vnd.api+json": { "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } - } - }, - "application/*+json": { - "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" - } + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" } } } }, "responses": { - "200": { + "204": { "description": "Success" } } }, "patch": { "tags": [ - "Flights" + "flights" ], + "operationId": "patch-flight-flight-attendants-relationship", "parameters": [ { "name": "id", @@ -791,39 +753,28 @@ "type": "integer", "format": "int32" } - }, - { - "name": "relationshipName", - "in": "path", - "required": true, - "schema": { - "type": "string" - } } ], "requestBody": { "content": { - "application/json": { - "schema": {} - }, - "text/json": { - "schema": {} - }, - "application/*+json": { - "schema": {} - } + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + } + } } }, "responses": { - "200": { + "204": { "description": "Success" } } }, "delete": { "tags": [ - "Flights" + "flights" ], + "operationId": "delete-flight-flight-attendants-relationship", "parameters": [ { "name": "id", @@ -833,49 +784,321 @@ "type": "integer", "format": "int32" } - }, + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/flights/{id}/purser": { + "get": { + "tags": [ + "flights" + ], + "operationId": "get-flight-purser", + "parameters": [ { - "name": "relationshipName", + "name": "id", "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-secondary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flights" + ], + "operationId": "head-flight-purser", + "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/flight-attendant-secondary-response-document" + } + } + } + } + } + } + }, + "/api/v1/flights/{id}/relationships/purser": { + "get": { + "tags": [ + "flights" + ], + "operationId": "get-flight-purser-relationship", + "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/flight-attendant-identifier-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flights" + ], + "operationId": "head-flight-purser-relationship", + "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/flight-attendant-identifier-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "flights" + ], + "operationId": "patch-flight-purser-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" } } ], "requestBody": { "content": { - "application/json": { + "application/vnd.api+json": { "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" + "$ref": "#/components/schemas/to-one-flight-attendant-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/flights/{id}/servicing-airplane": { + "get": { + "tags": [ + "flights" + ], + "operationId": "get-flight-servicing-airplane", + "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/airplane-secondary-response-document" } } - }, - "text/json": { - "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" + } + } + } + }, + "head": { + "tags": [ + "flights" + ], + "operationId": "head-flight-servicing-airplane", + "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/airplane-secondary-response-document" } } - }, - "application/*+json": { - "schema": { - "uniqueItems": true, - "type": "array", - "items": { - "$ref": "#/components/schemas/IIdentifiable" + } + } + } + } + }, + "/api/v1/flights/{id}/relationships/servicing-airplane": { + "get": { + "tags": [ + "flights" + ], + "operationId": "get-flight-servicing-airplane-relationship", + "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/airplane-identifier-response-document" } } } } - }, + } + }, + "head": { + "tags": [ + "flights" + ], + "operationId": "head-flight-servicing-airplane-relationship", + "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/airplane-identifier-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "flights" + ], + "operationId": "patch-flight-servicing-airplane-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-one-airplane-request-data" + } + } + } + }, + "responses": { + "204": { "description": "Success" } } @@ -884,76 +1107,801 @@ }, "components": { "schemas": { - "Airplane": { + "airplane-attributes-in-response": { "type": "object", "properties": { "id": { - "type": "integer", - "format": "int32" - }, - "stringId": { - "type": "string", - "nullable": true - }, - "localId": { "type": "string", "nullable": true + } + }, + "additionalProperties": false + }, + "airplane-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} }, - "seatingCapacity": { - "type": "integer", - "format": "int32" + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" }, - "manufacturedAt": { - "type": "string", - "format": "date-time" + "links": { + "$ref": "#/components/schemas/links-in-resource-collection-document" }, - "flights": { - "uniqueItems": true, + "data": { "type": "array", "items": { - "$ref": "#/components/schemas/Flight" - }, - "nullable": true + "$ref": "#/components/schemas/airplane-data-in-response" + } } }, "additionalProperties": false }, - "Flight": { + "airplane-data-in-patch-request": { + "required": [ + "id", + "type" + ], "type": "object", "properties": { - "id": { - "type": "integer", - "format": "int32" + "type": { + "$ref": "#/components/schemas/airplanes-resource-type" }, - "stringId": { - "type": "string", - "nullable": true + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "airplane-data-in-post-request": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/airplanes-resource-type" + } + }, + "additionalProperties": false + }, + "airplane-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/airplanes-resource-type" }, - "localId": { - "type": "string", - "nullable": true + "id": { + "type": "string" }, - "destination": { - "type": "string", - "nullable": true + "attributes": { + "$ref": "#/components/schemas/airplane-attributes-in-response" }, - "departsAt": { - "type": "string", - "format": "date-time" + "links": { + "$ref": "#/components/schemas/links-in-resource-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} } }, "additionalProperties": false }, - "IIdentifiable": { + "airplane-identifier": { + "required": [ + "id", + "type" + ], "type": "object", "properties": { - "stringId": { - "type": "string", - "nullable": true + "type": { + "type": "string" }, - "localId": { - "type": "string", - "nullable": true + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "airplane-identifier-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/airplane-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "airplane-patch-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/airplane-data-in-patch-request" + } + }, + "additionalProperties": false + }, + "airplane-post-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/airplane-data-in-post-request" + } + }, + "additionalProperties": false + }, + "airplane-primary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + }, + "data": { + "$ref": "#/components/schemas/airplane-data-in-response" + } + }, + "additionalProperties": false + }, + "airplane-secondary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/airplane-data-in-response" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "airplanes-resource-type": { + "enum": [ + "airplanes" + ], + "type": "string" + }, + "flight-attendant-attributes-in-response": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + } + }, + "additionalProperties": false + }, + "flight-attendant-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-collection-document" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + } + } + }, + "additionalProperties": false + }, + "flight-attendant-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/flight-attendants-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/flight-attendant-attributes-in-response" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "flight-attendant-identifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "flight-attendant-identifier-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + } + }, + "additionalProperties": false + }, + "flight-attendant-identifier-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "flight-attendant-secondary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "flight-attendants-resource-type": { + "enum": [ + "flight-attendants" + ], + "type": "string" + }, + "flight-attributes-in-response": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + "flight-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-collection-document" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-data-in-response" + } + } + }, + "additionalProperties": false + }, + "flight-data-in-patch-request": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/flights-resource-type" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "flight-data-in-post-request": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/flights-resource-type" + } + }, + "additionalProperties": false + }, + "flight-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/flights-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/flight-attributes-in-response" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "flight-identifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "flight-identifier-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-identifier" + } + } + }, + "additionalProperties": false + }, + "flight-patch-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/flight-data-in-patch-request" + } + }, + "additionalProperties": false + }, + "flight-post-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/flight-data-in-post-request" + } + }, + "additionalProperties": false + }, + "flight-primary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + }, + "data": { + "$ref": "#/components/schemas/flight-data-in-response" + } + }, + "additionalProperties": false + }, + "flights-resource-type": { + "enum": [ + "flights" + ], + "type": "string" + }, + "jsonapi-object": { + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "ext": { + "type": "array", + "items": { + "type": "string" + } + }, + "profile": { + "type": "array", + "items": { + "type": "string" + } + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "links-in-resource-collection-document": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-document": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-identifier-collection-document": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "type": "string" + }, + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-identifier-document": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "type": "string" + } + }, + "additionalProperties": false + }, + "links-in-resource-object": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + } + }, + "additionalProperties": false + }, + "null-value": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": {} + }, + "nullable": true + }, + "to-many-flight-attendant-request-data": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + } + }, + "additionalProperties": false + }, + "to-many-flight-request-data": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-identifier" + } + } + }, + "additionalProperties": false + }, + "to-one-airplane-request-data": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/airplane-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "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 From 09ab5c0430060485ebb659f6d1a1b6c7fb925657 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 9 Sep 2021 10:39:42 +0200 Subject: [PATCH 03/35] Add FlightAttendantController to used controllers --- .../FlightAttendantsController.cs | 15 + test/OpenApiTests/OpenApiDocumentTests.cs | 1 + test/OpenApiTests/swagger.json | 709 ++++++++++++++++++ 3 files changed, 725 insertions(+) create mode 100644 test/OpenApiTests/FlightAttendantsController.cs diff --git a/test/OpenApiTests/FlightAttendantsController.cs b/test/OpenApiTests/FlightAttendantsController.cs new file mode 100644 index 0000000000..a19f267c72 --- /dev/null +++ b/test/OpenApiTests/FlightAttendantsController.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace OpenApiTests +{ + public sealed class FlightAttendantsController : JsonApiController + { + public FlightAttendantsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/OpenApiTests/OpenApiDocumentTests.cs b/test/OpenApiTests/OpenApiDocumentTests.cs index 898987712b..0711bb2f04 100644 --- a/test/OpenApiTests/OpenApiDocumentTests.cs +++ b/test/OpenApiTests/OpenApiDocumentTests.cs @@ -15,6 +15,7 @@ public OpenApiDocumentTests() { UseController(); UseController(); + UseController(); } [Fact] diff --git a/test/OpenApiTests/swagger.json b/test/OpenApiTests/swagger.json index 16a1e222b8..c2e54f5a45 100644 --- a/test/OpenApiTests/swagger.json +++ b/test/OpenApiTests/swagger.json @@ -398,6 +398,561 @@ } } }, + "/api/v1/flight-attendants": { + "get": { + "tags": [ + "flight-attendants" + ], + "operationId": "get-flight-attendant-collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flight-attendants" + ], + "operationId": "head-flight-attendant-collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "flight-attendants" + ], + "operationId": "post-flight-attendant", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-post-request-document" + } + } + } + }, + "responses": { + "201": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/flight-attendants/{id}": { + "get": { + "tags": [ + "flight-attendants" + ], + "operationId": "get-flight-attendant", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-primary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flight-attendants" + ], + "operationId": "head-flight-attendant", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-primary-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "flight-attendants" + ], + "operationId": "patch-flight-attendant", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-patch-request-document" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-attendant-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "flight-attendants" + ], + "operationId": "delete-flight-attendant", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/flight-attendants/{id}/flights": { + "get": { + "tags": [ + "flight-attendants" + ], + "operationId": "get-flight-attendant-flights", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flight-attendants" + ], + "operationId": "head-flight-attendant-flights", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-collection-response-document" + } + } + } + } + } + } + }, + "/api/v1/flight-attendants/{id}/relationships/flights": { + "get": { + "tags": [ + "flight-attendants" + ], + "operationId": "get-flight-attendant-flights-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-identifier-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flight-attendants" + ], + "operationId": "head-flight-attendant-flights-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-identifier-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "flight-attendants" + ], + "operationId": "post-flight-attendant-flights-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "flight-attendants" + ], + "operationId": "patch-flight-attendant-flights-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "flight-attendants" + ], + "operationId": "delete-flight-attendant-flights-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/flight-attendants/{id}/purser-on-flight": { + "get": { + "tags": [ + "flight-attendants" + ], + "operationId": "get-flight-attendant-purser-on-flight", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-secondary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flight-attendants" + ], + "operationId": "head-flight-attendant-purser-on-flight", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-secondary-response-document" + } + } + } + } + } + } + }, + "/api/v1/flight-attendants/{id}/relationships/purser-on-flight": { + "get": { + "tags": [ + "flight-attendants" + ], + "operationId": "get-flight-attendant-purser-on-flight-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-identifier-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "flight-attendants" + ], + "operationId": "head-flight-attendant-purser-on-flight-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/flight-identifier-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "flight-attendants" + ], + "operationId": "patch-flight-attendant-purser-on-flight-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-one-flight-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, "/api/v1/flights": { "get": { "tags": [ @@ -1363,6 +1918,34 @@ }, "additionalProperties": false }, + "flight-attendant-data-in-patch-request": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/flight-attendants-resource-type" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "flight-attendant-data-in-post-request": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/flight-attendants-resource-type" + } + }, + "additionalProperties": false + }, "flight-attendant-data-in-response": { "required": [ "id", @@ -1462,6 +2045,53 @@ }, "additionalProperties": false }, + "flight-attendant-patch-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/flight-attendant-data-in-patch-request" + } + }, + "additionalProperties": false + }, + "flight-attendant-post-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/flight-attendant-data-in-post-request" + } + }, + "additionalProperties": false + }, + "flight-attendant-primary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + }, + "data": { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + } + }, + "additionalProperties": false + }, "flight-attendant-secondary-response-document": { "required": [ "data", @@ -1631,6 +2261,36 @@ }, "additionalProperties": false }, + "flight-identifier-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, "flight-patch-request-document": { "required": [ "data" @@ -1678,6 +2338,36 @@ }, "additionalProperties": false }, + "flight-secondary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-data-in-response" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, "flights-resource-type": { "enum": [ "flights" @@ -1905,6 +2595,25 @@ } }, "additionalProperties": false + }, + "to-one-flight-request-data": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false } } } From 19cc14d6d2f7876b4b673ac6b88ed532f45299a6 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 9 Sep 2021 23:10:10 +0200 Subject: [PATCH 04/35] Added client library with tests to project; made adjustmets to the models to make the client test pass without introducing any structural adjustments to the test codes --- JsonApiDotNetCore.sln | 30 + src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs | 3 + .../JsonApiDataContractResolver.cs | 16 +- .../IJsonApiClient.cs | 38 + .../JsonApiClient.cs | 224 +++++ .../JsonApiDotNetCore.OpenApiClient.csproj | 35 + .../Properties/AssemblyInfo.cs | 2 + src/JsonApiDotNetCore/TypeExtensions.cs | 29 - test/OpenApiTests/AircraftType.cs | 3 + test/OpenApiTests/Airline.cs | 12 + test/OpenApiTests/Airplane.cs | 34 +- .../OpenApiTests/ClientLibrary/ApiResponse.cs | 33 + ...lientAttributeRegistrationLifeTimeTests.cs | 440 +++++++++ .../GeneratedCode/IOpenApiClient.cs | 8 + .../GeneratedCode/OpenApiClient.cs | 17 + .../ClientLibrary/RequestTests.cs | 492 ++++++++++ .../ClientLibrary/ResponseTests.cs | 604 +++++++++++++ test/OpenApiTests/Flight.cs | 48 +- test/OpenApiTests/FlightAttendant.cs | 19 +- test/OpenApiTests/FlightAttendantExpertise.cs | 3 + .../FlightAttendantsController.cs | 4 +- test/OpenApiTests/OpenApiDbContext.cs | 11 +- test/OpenApiTests/OpenApiTests.csproj | 23 + test/OpenApiTests/swagger.json | 839 +++++++++++++----- .../FakeHttpClientWrapper.cs | 107 +++ .../HttpRequestHeadersExtensions.cs | 21 + 26 files changed, 2811 insertions(+), 284 deletions(-) create mode 100644 src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs create mode 100644 src/JsonApiDotNetCore.OpenApiClient/IJsonApiClient.cs create mode 100644 src/JsonApiDotNetCore.OpenApiClient/JsonApiClient.cs create mode 100644 src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj create mode 100644 test/OpenApiTests/Airline.cs create mode 100644 test/OpenApiTests/ClientLibrary/ApiResponse.cs create mode 100644 test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs create mode 100644 test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs create mode 100644 test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs create mode 100644 test/OpenApiTests/ClientLibrary/RequestTests.cs create mode 100644 test/OpenApiTests/ClientLibrary/ResponseTests.cs create mode 100644 test/TestBuildingBlocks/FakeHttpClientWrapper.cs create mode 100644 test/TestBuildingBlocks/HttpRequestHeadersExtensions.cs diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln index fcb0603b0e..4adbd778fb 100644 --- a/JsonApiDotNetCore.sln +++ b/JsonApiDotNetCore.sln @@ -48,6 +48,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiTests", "test\OpenApiTests\OpenApiTests.csproj", "{B693DE14-BB28-496F-AB39-B4E674ABCA80}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiClient", "src\Examples\OpenApiClient\OpenApiClient.csproj", "{63C2C6C1-0967-4439-BB63-A55DA22869AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApiClient", "src\JsonApiDotNetCore.OpenApiClient\JsonApiDotNetCore.OpenApiClient.csproj", "{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -238,6 +242,30 @@ Global {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x64.Build.0 = Release|Any CPU {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.ActiveCfg = Release|Any CPU {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.Build.0 = Release|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x64.Build.0 = Debug|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x86.Build.0 = Debug|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|Any CPU.Build.0 = Release|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x64.ActiveCfg = Release|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x64.Build.0 = Release|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x86.ActiveCfg = Release|Any CPU + {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x86.Build.0 = Release|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x64.ActiveCfg = Debug|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x64.Build.0 = Debug|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x86.ActiveCfg = Debug|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x86.Build.0 = Debug|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|Any CPU.Build.0 = Release|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x64.ActiveCfg = Release|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x64.Build.0 = Release|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x86.ActiveCfg = Release|Any CPU + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -258,6 +286,8 @@ Global {210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {71287D6F-6C3B-44B4-9FCA-E78FE3F02289} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} {B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} + {63C2C6C1-0967-4439-BB63-A55DA22869AB} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} diff --git a/src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs b/src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs new file mode 100644 index 0000000000..9bb84b072a --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApiClient")] diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs index 97129f14ee..ff707e1614 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs @@ -44,11 +44,7 @@ public DataContract GetDataContractForType(Type type) IList replacementProperties = null; - if (type.IsSubclassOfOpenGeneric(typeof(Identifiable<>))) - { - replacementProperties = dataContract.ObjectProperties.Where(IsIdentity).ToArray(); - } - else if (type.IsAssignableTo(typeof(IIdentifiable))) + if (type.IsAssignableTo(typeof(IIdentifiable))) { replacementProperties = GetDataPropertiesThatExistInResourceContext(type, dataContract); } @@ -61,6 +57,16 @@ public DataContract GetDataContractForType(Type type) return dataContract; } + private static bool IsIdentifiableBaseType(Type type) + { + if (type.IsGenericType) + { + return type.GetGenericTypeDefinition() == typeof(Identifiable<>); + } + + return type == typeof(Identifiable); + } + private static bool IsIdentity(DataProperty property) { return property.MemberInfo.Name == nameof(Identifiable.Id); diff --git a/src/JsonApiDotNetCore.OpenApiClient/IJsonApiClient.cs b/src/JsonApiDotNetCore.OpenApiClient/IJsonApiClient.cs new file mode 100644 index 0000000000..39eabca632 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApiClient/IJsonApiClient.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq.Expressions; + +namespace JsonApiDotNetCore.OpenApiClient +{ + 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 . + /// + /// + /// The request document instance for which this registration applies. + /// + /// + /// Optional. A list of expressions to indicate which properties to unconditionally include in the JSON request body. For example: + /// video.Title, video => video.Summary + /// ]]> + /// + /// + /// The type of the request document. + /// + /// + /// The type of the attributes object inside . + /// + /// + /// 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, + params Expression>[] alwaysIncludedAttributeSelectors) + where TRequestDocument : class; + } +} diff --git a/src/JsonApiDotNetCore.OpenApiClient/JsonApiClient.cs b/src/JsonApiDotNetCore.OpenApiClient/JsonApiClient.cs new file mode 100644 index 0000000000..39112e98dd --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApiClient/JsonApiClient.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using JetBrains.Annotations; +using JsonApiDotNetCore.OpenApi; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace JsonApiDotNetCore.OpenApiClient +{ + /// + /// Base class to inherit auto-generated client from. Enables to mark fields to be explicitly included in a request body, even if they are null or + /// default. + /// + [PublicAPI] + public abstract class JsonApiClient : IJsonApiClient + { + private readonly JsonApiJsonConverter _jsonApiJsonConverter = new(); + + protected void SetSerializerSettingsForJsonApi(JsonSerializerSettings settings) + { + ArgumentGuard.NotNull(settings, nameof(settings)); + + settings.Converters.Add(_jsonApiJsonConverter); + } + + /// + public IDisposable RegisterAttributesForRequestDocument(TRequestDocument requestDocument, + params Expression>[] alwaysIncludedAttributeSelectors) + where TRequestDocument : class + { + ArgumentGuard.NotNull(requestDocument, nameof(requestDocument)); + + var attributeNames = new HashSet(); + + foreach (Expression> selector in alwaysIncludedAttributeSelectors) + { + if (RemoveConvert(selector.Body) is MemberExpression selectorBody) + { + attributeNames.Add(selectorBody.Member.Name); + } + else + { + throw new ArgumentException($"The expression '{selector}' should select a single property. For example: 'article => article.Title'."); + } + } + + _jsonApiJsonConverter.RegisterRequestDocument(requestDocument, new AttributeNamesContainer(attributeNames, typeof(TAttributesObject))); + + return new AttributesRegistrationScope(_jsonApiJsonConverter, requestDocument); + } + + private static Expression RemoveConvert(Expression expression) + { + Expression innerExpression = expression; + + while (true) + { + if (innerExpression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression) + { + innerExpression = unaryExpression.Operand; + } + else + { + return innerExpression; + } + } + } + + private sealed class JsonApiJsonConverter : JsonConverter + { + private readonly Dictionary _alwaysIncludedAttributesPerRequestDocumentInstance = new(); + private readonly Dictionary> _requestDocumentInstancesPerRequestDocumentType = new(); + private bool _isSerializing; + + public override bool CanRead => false; + + public void RegisterRequestDocument(object requestDocument, AttributeNamesContainer attributes) + { + _alwaysIncludedAttributesPerRequestDocumentInstance[requestDocument] = attributes; + + Type requestDocumentType = requestDocument.GetType(); + + if (!_requestDocumentInstancesPerRequestDocumentType.ContainsKey(requestDocumentType)) + { + _requestDocumentInstancesPerRequestDocumentType[requestDocumentType] = new HashSet(); + } + + _requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Add(requestDocument); + } + + public void RemoveAttributeRegistration(object requestDocument) + { + if (_alwaysIncludedAttributesPerRequestDocumentInstance.ContainsKey(requestDocument)) + { + _alwaysIncludedAttributesPerRequestDocumentInstance.Remove(requestDocument); + + Type requestDocumentType = requestDocument.GetType(); + _requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Remove(requestDocument); + + if (!_requestDocumentInstancesPerRequestDocumentType[requestDocumentType].Any()) + { + _requestDocumentInstancesPerRequestDocumentType.Remove(requestDocumentType); + } + } + } + + public override bool CanConvert(Type objectType) + { + ArgumentGuard.NotNull(objectType, nameof(objectType)); + + return !_isSerializing && _requestDocumentInstancesPerRequestDocumentType.ContainsKey(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new UnreachableCodeException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + ArgumentGuard.NotNull(writer, nameof(writer)); + ArgumentGuard.NotNull(value, nameof(value)); + ArgumentGuard.NotNull(serializer, nameof(serializer)); + + if (_alwaysIncludedAttributesPerRequestDocumentInstance.ContainsKey(value)) + { + AttributeNamesContainer attributeNamesContainer = _alwaysIncludedAttributesPerRequestDocumentInstance[value]; + serializer.ContractResolver = new JsonApiDocumentContractResolver(attributeNamesContainer); + } + + try + { + _isSerializing = true; + serializer.Serialize(writer, value); + } + finally + { + _isSerializing = false; + } + } + } + + private sealed class AttributeNamesContainer + { + private readonly ISet _attributeNames; + private readonly Type _containerType; + + public AttributeNamesContainer(ISet attributeNames, Type containerType) + { + ArgumentGuard.NotNull(attributeNames, nameof(attributeNames)); + ArgumentGuard.NotNull(containerType, nameof(containerType)); + + _attributeNames = attributeNames; + _containerType = containerType; + } + + public bool ContainsAttribute(string name) + { + return _attributeNames.Contains(name); + } + + public bool ContainerMatchesType(Type type) + { + return _containerType == type; + } + } + + private sealed class AttributesRegistrationScope : IDisposable + { + private readonly JsonApiJsonConverter _jsonApiJsonConverter; + private readonly object _requestDocument; + + public AttributesRegistrationScope(JsonApiJsonConverter jsonApiJsonConverter, object requestDocument) + { + ArgumentGuard.NotNull(jsonApiJsonConverter, nameof(jsonApiJsonConverter)); + ArgumentGuard.NotNull(requestDocument, nameof(requestDocument)); + + _jsonApiJsonConverter = jsonApiJsonConverter; + _requestDocument = requestDocument; + } + + public void Dispose() + { + _jsonApiJsonConverter.RemoveAttributeRegistration(_requestDocument); + } + } + + private sealed class JsonApiDocumentContractResolver : DefaultContractResolver + { + private readonly AttributeNamesContainer _attributeNamesContainer; + + public JsonApiDocumentContractResolver(AttributeNamesContainer attributeNamesContainer) + { + ArgumentGuard.NotNull(attributeNamesContainer, nameof(attributeNamesContainer)); + + _attributeNamesContainer = attributeNamesContainer; + } + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + JsonProperty property = base.CreateProperty(member, memberSerialization); + + if (_attributeNamesContainer.ContainerMatchesType(property.DeclaringType)) + { + if (_attributeNamesContainer.ContainsAttribute(property.UnderlyingName)) + { + property.NullValueHandling = NullValueHandling.Include; + property.DefaultValueHandling = DefaultValueHandling.Include; + } + else + { + property.NullValueHandling = NullValueHandling.Ignore; + property.DefaultValueHandling = DefaultValueHandling.Ignore; + } + } + + return property; + } + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj b/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj new file mode 100644 index 0000000000..615f85a16e --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj @@ -0,0 +1,35 @@ + + + $(JsonApiDotNetCoreVersionPrefix) + $(NetCoreAppVersion) + true + + + + jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net;openapi;swagger;client;nswag + TODO + json-api-dotnet + https://www.jsonapi.net/ + MIT + false + true + true + embedded + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs index 2cc2ee88bc..bb49f8748a 100644 --- a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs @@ -1,8 +1,10 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApi")] +[assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApiClient")] [assembly: InternalsVisibleTo("Benchmarks")] [assembly: InternalsVisibleTo("JsonApiDotNetCoreTests")] [assembly: InternalsVisibleTo("UnitTests")] [assembly: InternalsVisibleTo("DiscoveryTests")] +[assembly: InternalsVisibleTo("OpenApiTests")] [assembly: InternalsVisibleTo("TestBuildingBlocks")] diff --git a/src/JsonApiDotNetCore/TypeExtensions.cs b/src/JsonApiDotNetCore/TypeExtensions.cs index df41d66840..d25599b821 100644 --- a/src/JsonApiDotNetCore/TypeExtensions.cs +++ b/src/JsonApiDotNetCore/TypeExtensions.cs @@ -19,34 +19,5 @@ public static bool IsOrImplementsInterface(this Type source, Type interfaceType) return source == interfaceType || source.GetInterfaces().Any(type => type == interfaceType); } - - /// - /// Whether the specified source type inherits from the specified open generic type. - /// - internal static bool IsSubclassOfOpenGeneric(this Type source, Type openGenericType) - { - ArgumentGuard.NotNull(openGenericType, nameof(openGenericType)); - ArgumentGuard.NotNull(source, nameof(openGenericType)); - - // TODO: check if source should be allowed null and return false in that case? - - Type typeInInheritanceTreeOfSource = source; - - while (typeInInheritanceTreeOfSource != null && typeInInheritanceTreeOfSource != typeof(object)) - { - Type typeToCheck = typeInInheritanceTreeOfSource.IsGenericType - ? typeInInheritanceTreeOfSource.GetGenericTypeDefinition() - : typeInInheritanceTreeOfSource; - - if (openGenericType == typeToCheck) - { - return true; - } - - typeInInheritanceTreeOfSource = typeInInheritanceTreeOfSource.BaseType; - } - - return false; - } } } diff --git a/test/OpenApiTests/AircraftType.cs b/test/OpenApiTests/AircraftType.cs index bd132d5acc..7cf37c9b25 100644 --- a/test/OpenApiTests/AircraftType.cs +++ b/test/OpenApiTests/AircraftType.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace OpenApiTests { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum AircraftType { Turboprops, diff --git a/test/OpenApiTests/Airline.cs b/test/OpenApiTests/Airline.cs new file mode 100644 index 0000000000..86e1b94ad0 --- /dev/null +++ b/test/OpenApiTests/Airline.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; + +namespace OpenApiTests +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public enum Airline + { + DeltaAirLines, + LufthansaGroup, + AirFranceKlm + } +} diff --git a/test/OpenApiTests/Airplane.cs b/test/OpenApiTests/Airplane.cs index 35e1f93454..a4c55b30ac 100644 --- a/test/OpenApiTests/Airplane.cs +++ b/test/OpenApiTests/Airplane.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -9,22 +10,39 @@ namespace OpenApiTests [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Airplane : Identifiable { - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] - public int SeatingCapacity { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] - public long SerialNumber { get; set; } - [Attr(PublicName = "airplane-type", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] - public AircraftType AircraftType { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] + [Required] + [MaxLength(255)] + public string Name { get; set; } - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] public DateTime ManufacturedAt { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] + public int? AirtimeInHours { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] public DateTime? LastServicedAt { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + public bool IsInMaintenance { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + [MaxLength(2000)] + public string SerialNumber { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + public int SeatingCapacity { get; set; } + + [Attr(PublicName = "airplane-type", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + public AircraftType AircraftType { get; set; } + [HasMany] public ISet Flights { get; set; } } } + + + + diff --git a/test/OpenApiTests/ClientLibrary/ApiResponse.cs b/test/OpenApiTests/ClientLibrary/ApiResponse.cs new file mode 100644 index 0000000000..f2b1fbb293 --- /dev/null +++ b/test/OpenApiTests/ClientLibrary/ApiResponse.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using JsonApiDotNetCore; +using OpenApiTests.ClientLibrary.GeneratedCode; + +#pragma warning disable AV1008 // Class should not be static + +namespace OpenApiTests.ClientLibrary +{ + internal static class ApiResponse + { + public static async Task TranslateAsync(Func> operation) + { + // Workaround for https://github.com/RicoSuter/NSwag/issues/2499 + + ArgumentGuard.NotNull(operation, nameof(operation)); + + try + { + return await operation(); + } + catch (ApiException exception) + { + if (exception.StatusCode != 204) + { + throw; + } + + return default; + } + } + } +} diff --git a/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs new file mode 100644 index 0000000000..3f4463b60a --- /dev/null +++ b/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs @@ -0,0 +1,440 @@ +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Common; +using FluentAssertions.Extensions; +using OpenApiTests.ClientLibrary.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +#pragma warning disable AV1704 // Don't include numbers in variables, parameters and type members + +namespace OpenApiTests.ClientLibrary +{ + public sealed class ClientAttributeRegistrationLifetimeTests + { + [Fact] + public async Task Disposed_attribute_registration_for_document_does_not_affect_request() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + const string airplaneId = "XUuiP"; + var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); + + var requestDocument = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest() + { + ManufacturedAt = manufacturedAt + } + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + airplane => airplane.AirtimeInHours)) + { + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + } + + wrapper.ChangeResponse(HttpStatusCode.NoContent, null); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + + // Assert + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + airplaneId + @""", + ""attributes"": { + ""manufactured-at"": ""2021-01-01T15:23:05.033+04:00"", + ""is-in-maintenance"": false + } + } +}"); + } + + [Fact] + public async Task Attribute_registration_can_be_used_for_multiple_requests() + { + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Arrange + const string airplaneId = "XUuiP"; + + var requestDocument = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest + { + AirtimeInHours = 100 + } + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + airplane => airplane.AirtimeInHours)) + { + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + + wrapper.ChangeResponse(HttpStatusCode.NoContent, null); + + requestDocument.Data.Attributes.AirtimeInHours = null; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + } + + // Assert + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + airplaneId + @""", + ""attributes"": { + ""airtime-in-hours"": null + } + } +}"); + } + + [Fact] + public async Task Request_is_unaffected_by_attribute_registration_for_different_document_of_same_type() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + const string airplaneId1 = "XUuiP"; + var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); + + var requestDocument1 = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId1, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest() + } + }; + + const string airplaneId2 = "DJy1u"; + + var requestDocument2 = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId2, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest + { + ManufacturedAt = manufacturedAt + } + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + airplane => airplane.AirtimeInHours)) + { + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + airplane => airplane.ManufacturedAt)) + { + } + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + } + + // Assert + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + airplaneId2 + @""", + ""attributes"": { + ""manufactured-at"": ""2021-01-01T15:23:05.033+04:00"", + ""is-in-maintenance"": false + } + } +}"); + } + + [Fact] + public async Task Attribute_values_can_be_changed_after_attribute_registration() + { + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Arrange + const string airplaneId = "XUuiP"; + + var requestDocument = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest + { + IsInMaintenance = true + } + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + airplane => airplane.IsInMaintenance)) + { + requestDocument.Data.Attributes.IsInMaintenance = false; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + } + + // Assert + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + airplaneId + @""", + ""attributes"": { + ""is-in-maintenance"": false + } + } +}"); + } + + [Fact] + public async Task Attribute_registration_is_unaffected_by_successive_attribute_registration_for_document_of_different_type() + { + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Arrange + const string airplaneId1 = "XUuiP"; + + var requestDocument1 = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId1, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest() + } + }; + + var requestDocument2 = new AirplanePostRequestDocument + { + Data = new AirplaneDataInPostRequest + { + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPostRequest() + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + airplane => airplane.IsInMaintenance)) + { + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + airplane => airplane.AirtimeInHours)) + { + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); + } + } + + // Assert + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + airplaneId1 + @""", + ""attributes"": { + ""is-in-maintenance"": false + } + } +}"); + } + + [Fact] + public async Task Attribute_registration_is_unaffected_by_preceding_disposed_attribute_registration_for_different_document_of_same_type() + { + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Arrange + const string airplaneId1 = "XUuiP"; + + var requestDocument1 = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId1, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest() + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + airplane => airplane.AirtimeInHours)) + { + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); + } + + const string airplaneId2 = "DJy1u"; + + var requestDocument2 = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId2, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest + { + SerialNumber = "100" + } + } + }; + + wrapper.ChangeResponse(HttpStatusCode.NoContent, null); + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + airplane => airplane.LastServicedAt)) + { + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + } + + // Assert + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + airplaneId2 + @""", + ""attributes"": { + ""last-serviced-at"": null, + ""serial-number"": ""100"" + } + } +}"); + } + + [Fact] + public async Task Attribute_registration_is_unaffected_by_preceding_disposed_attribute_registration_for_document_of_different_type() + { + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Arrange + var requestDocument1 = new AirplanePostRequestDocument + { + Data = new AirplaneDataInPostRequest + { + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPostRequest() + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + airplane => airplane.AirtimeInHours)) + { + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument1)); + } + + const string airplaneId = "DJy1u"; + + var requestDocument2 = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest + { + SerialNumber = "100" + } + } + }; + + wrapper.ChangeResponse(HttpStatusCode.NoContent, null); + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + airplane => airplane.LastServicedAt)) + { + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument2)); + } + + // Assert + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + airplaneId + @""", + ""attributes"": { + ""last-serviced-at"": null, + ""serial-number"": ""100"" + } + } +}"); + } + + [Fact] + public async Task Attribute_registration_is_unaffected_by_preceding_attribute_registration_for_different_document_of_same_type() + { + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Arrange + const string airplaneId1 = "XUuiP"; + + var requestDocument1 = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId1, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest() + } + }; + + const string airplaneId2 = "DJy1u"; + + var requestDocument2 = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId2, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest() + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + airplane => airplane.ManufacturedAt)) + { + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) + { + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + } + } + + // Assert + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + airplaneId2 + @""", + ""attributes"": { + ""airtime-in-hours"": null, + ""is-in-maintenance"": false + } + } +}"); + } + } +} diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs b/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs new file mode 100644 index 0000000000..394714bc32 --- /dev/null +++ b/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs @@ -0,0 +1,8 @@ +using JsonApiDotNetCore.OpenApiClient; + +namespace OpenApiTests.ClientLibrary.GeneratedCode +{ + partial interface IOpenApiClient : IJsonApiClient + { + } +} diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs b/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs new file mode 100644 index 0000000000..bf94af876c --- /dev/null +++ b/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs @@ -0,0 +1,17 @@ +using JsonApiDotNetCore.OpenApiClient; +using Newtonsoft.Json; + +namespace OpenApiTests.ClientLibrary.GeneratedCode +{ + public partial class OpenApiClient : JsonApiClient + { + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + +#if DEBUG + settings.Formatting = Formatting.Indented; +#endif + } + } +} diff --git a/test/OpenApiTests/ClientLibrary/RequestTests.cs b/test/OpenApiTests/ClientLibrary/RequestTests.cs new file mode 100644 index 0000000000..4dd60e400e --- /dev/null +++ b/test/OpenApiTests/ClientLibrary/RequestTests.cs @@ -0,0 +1,492 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Common; +using FluentAssertions.Extensions; +using JsonApiDotNetCore.Middleware; +using Microsoft.Net.Http.Headers; +using OpenApiTests.ClientLibrary.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +#pragma warning disable AV1500 // Member or local function contains too many statements + +namespace OpenApiTests.ClientLibrary +{ + public sealed class RequestTests + { + private const string HostPrefix = "http://localhost/api/v1/"; + + [Fact] + public async Task Getting_resource_collection_produces_expected_request() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCollectionAsync()); + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Get); + wrapper.Request.RequestUri.Should().Be(HostPrefix + "flights"); + wrapper.RequestBody.Should().BeNull(); + } + + [Fact] + public async Task Getting_resource_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightAsync(flightId)); + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Get); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}"); + wrapper.RequestBody.Should().BeNull(); + } + + [Fact] + public async Task Partial_posting_resource_with_selected_relationships_produces_expected_request() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + var requestDocument = new FlightPostRequestDocument + { + Data = new FlightDataInPostRequest + { + Type = FlightsResourceType.Flights, + Relationships = new FlightRelationshipsInPostRequest + { + OperatingAirplane = new ToOneAirplaneRequestData() + } + } + }; + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostFlightAsync(requestDocument)); + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(HostPrefix + "flights"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""flights"", + ""relationships"": { + ""operating-airplane"": { + ""data"": null + } + } + } +}"); + } + + [Fact] + public async Task Partial_posting_resource_produces_expected_request() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + const char euroSign = '\x20AC'; + const char checkMark = '\x2713'; + const char capitalLWithStroke = '\x0141'; + + string specialCharacters = new(new[] + { + euroSign, + checkMark, + capitalLWithStroke + }); + + string name = "anAirplaneName " + specialCharacters; + + var requestDocument = new AirplanePostRequestDocument + { + Data = new AirplaneDataInPostRequest + { + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPostRequest + { + Name = name, + SeatingCapacity = 250 + } + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + airplane => airplane.AirtimeInHours)) + { + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument)); + } + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(HostPrefix + "airplanes"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""attributes"": { + ""name"": """ + name + @""", + ""airtime-in-hours"": null, + ""seating-capacity"": 250 + } + } +}"); + } + + [Fact] + public async Task Partial_patching_resource_produces_expected_request() + { + // Arrange + const string airplaneId = "XUuiP"; + var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + var requestDocument = new AirplanePatchRequestDocument + { + Data = new AirplaneDataInPatchRequest + { + Id = airplaneId, + Type = AirplanesResourceType.Airplanes, + Attributes = new AirplaneAttributesInPatchRequest + { + ManufacturedAt = manufacturedAt + } + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + airplane => airplane.ManufacturedAt, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) + { + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + } + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Patch); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"airplanes/{airplaneId}"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": ""XUuiP"", + ""attributes"": { + ""manufactured-at"": ""2021-01-01T15:23:05.033+04:00"", + ""airtime-in-hours"": null, + ""last-serviced-at"": null, + ""is-in-maintenance"": false + } + } +}"); + } + + [Fact] + public async Task Deleting_resource_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + await apiClient.DeleteFlightAsync(flightId); + + // Assert + wrapper.Request.Method.Should().Be(HttpMethod.Delete); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}"); + wrapper.RequestBody.Should().BeNull(); + } + + [Fact] + public async Task Getting_secondary_resource_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightOperatingAirplaneAsync(flightId)); + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Get); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/operating-airplane"); + wrapper.RequestBody.Should().BeNull(); + } + + [Fact] + public async Task Getting_secondary_resources_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightFlightAttendantsAsync(flightId)); + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Get); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/flight-attendants"); + wrapper.RequestBody.Should().BeNull(); + } + + [Fact] + public async Task Getting_ToOne_relationship_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId)); + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Get); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/operating-airplane"); + wrapper.RequestBody.Should().BeNull(); + } + + [Fact] + public async Task Patching_ToOne_relationship_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + var requestDocument = new ToOneAirplaneRequestData() + { + Data = new AirplaneIdentifier() + { + Id = "bBJHu", + Type = AirplanesResourceType.Airplanes + } + }; + + // Act + await apiClient.PatchFlightOperatingAirplaneRelationshipAsync(flightId, requestDocument); + + // Assert + wrapper.Request.Method.Should().Be(HttpMethod.Patch); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/operating-airplane"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": { + ""type"": ""airplanes"", + ""id"": ""bBJHu"" + } +}"); + } + + [Fact] + public async Task Getting_ToMany_relationship_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightFlightAttendantsRelationshipAsync(flightId)); + + // Assert + wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); + wrapper.Request.Method.Should().Be(HttpMethod.Get); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/flight-attendants"); + wrapper.RequestBody.Should().BeNull(); + } + + [Fact] + public async Task Posting_ToMany_relationship_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + var requestDocument = new ToManyFlightAttendantRequestData() + { + Data = new List + { + new() + { + Type = FlightAttendantsResourceType.FlightAttendants, + Id = "bBJHu" + }, + new() + { + Type = FlightAttendantsResourceType.FlightAttendants, + Id = "NInmX" + } + } + }; + + // Act + await apiClient.PostFlightFlightAttendantsRelationshipAsync(flightId, requestDocument); + + // Assert + wrapper.Request.Method.Should().Be(HttpMethod.Post); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/flight-attendants"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": [ + { + ""type"": ""flight-attendants"", + ""id"": ""bBJHu"" + }, + { + ""type"": ""flight-attendants"", + ""id"": ""NInmX"" + } + ] +}"); + } + + [Fact] + public async Task Patching_ToMany_relationship_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + var requestDocument = new ToManyFlightAttendantRequestData() + { + Data = new List + { + new() + { + Id = "bBJHu", + Type = FlightAttendantsResourceType.FlightAttendants + }, + new() + { + Id = "NInmX", + Type = FlightAttendantsResourceType.FlightAttendants + } + } + }; + + // Act + await apiClient.PatchFlightFlightAttendantsRelationshipAsync(flightId, requestDocument); + + // Assert + wrapper.Request.Method.Should().Be(HttpMethod.Patch); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/flight-attendants"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": [ + { + ""type"": ""flight-attendants"", + ""id"": ""bBJHu"" + }, + { + ""type"": ""flight-attendants"", + ""id"": ""NInmX"" + } + ] +}"); + } + + [Fact] + public async Task Deleting_ToMany_relationship_produces_expected_request() + { + // Arrange + const int flightId = 8712; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + var requestDocument = new ToManyFlightAttendantRequestData() + { + Data = new List + { + new() + { + Id = "bBJHu", + Type = FlightAttendantsResourceType.FlightAttendants + }, + new() + { + Id = "NInmX", + Type = FlightAttendantsResourceType.FlightAttendants + } + } + }; + + // Act + await apiClient.DeleteFlightFlightAttendantsRelationshipAsync(flightId, requestDocument); + + // Assert + wrapper.Request.Method.Should().Be(HttpMethod.Delete); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/flight-attendants"); + wrapper.Request.Content.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); + wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); + + wrapper.RequestBody.Should().BeJson(@"{ + ""data"": [ + { + ""type"": ""flight-attendants"", + ""id"": ""bBJHu"" + }, + { + ""type"": ""flight-attendants"", + ""id"": ""NInmX"" + } + ] +}"); + } + } +} diff --git a/test/OpenApiTests/ClientLibrary/ResponseTests.cs b/test/OpenApiTests/ClientLibrary/ResponseTests.cs new file mode 100644 index 0000000000..bc602a0a02 --- /dev/null +++ b/test/OpenApiTests/ClientLibrary/ResponseTests.cs @@ -0,0 +1,604 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using FluentAssertions; +using FluentAssertions.Specialized; +using OpenApiTests.ClientLibrary.GeneratedCode; +using TestBuildingBlocks; +using Xunit; + +#pragma warning disable AV1500 // Member or local function contains too many statements +#pragma warning disable AV1704 // Don't include numbers in variables, parameters and type members + +namespace OpenApiTests.ClientLibrary +{ + public sealed class ResponseTests + { + private const string HostPrefix = "http://localhost/api/v1/"; + + [Fact] + public async Task Getting_resource_collection_translates_response() + { + // Arrange + const string flightId = "8712"; + const string flightDestination = "Destination of Flight"; + const string fightServiceOnBoard = "Movies"; + const string flightDepartsAt = "2014-11-25T00:00:00"; + const string documentMetaValue = "1"; + const string flightMetaValue = "https://api.jsonapi.net/docs/#get-flights"; + const string operatingAirplaneMetaValue = "https://jsonapi.net/api/docs/#get-flight-operating-airplane"; + const string flightAttendantsMetaValue = "https://jsonapi.net/api/docs/#get-flight-flight-attendants"; + const string reserveFlightAttendantsMetaValue = "https://jsonapi.net/api/docs/#get-flight-reserve-flight-attendants"; + const string topLevelLink = HostPrefix + "flights"; + const string flightResourceLink = topLevelLink + "/" + flightId; + + const string responseBody = @"{ + ""meta"": { + ""total-resources"": """ + documentMetaValue + @""" + }, + ""links"": { + ""self"": """ + topLevelLink + @""", + ""first"": """ + topLevelLink + @""", + ""last"": """ + topLevelLink + @""" + }, + ""data"": [ + { + ""type"": ""flights"", + ""id"": """ + flightId + @""", + ""attributes"": { + ""destination"": """ + flightDestination + @""", + ""operated-by"": ""DeltaAirLines"", + ""departs-at"": """ + flightDepartsAt + @""", + ""arrives-at"": null, + ""services-on-board"": [ + """ + fightServiceOnBoard + @""", + """", + null + ] + }, + ""relationships"": { + ""operating-airplane"": { + ""links"": { + ""self"": """ + flightResourceLink + @"/relationships/operating-airplane"", + ""related"": """ + flightResourceLink + @"/operating-airplane"" + }, + ""meta"": { + ""docs"": """ + operatingAirplaneMetaValue + @""" + } + }, + ""flight-attendants"": { + ""links"": { + ""self"": """ + flightResourceLink + @"/relationships/flight-attendants"", + ""related"": """ + flightResourceLink + @"/flight-attendants"" + }, + ""meta"": { + ""docs"": """ + flightAttendantsMetaValue + @""" + } + }, + ""reserve-flight-attendants"": { + ""links"": { + ""self"": """ + flightResourceLink + @"/relationships/reserve-flight-attendants"", + ""related"": """ + flightResourceLink + @"/reserve-flight-attendants"" + }, + ""meta"": { + ""docs"": """ + reserveFlightAttendantsMetaValue + @""" + } + } + }, + ""links"": { + ""self"": """ + flightResourceLink + @""" + }, + ""meta"": { + ""docs"": """ + flightMetaValue + @""" + } + } + ] +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + FlightCollectionResponseDocument document = await apiClient.GetFlightCollectionAsync(); + + // Assert + document.Jsonapi.Should().BeNull(); + document.Meta.Should().HaveCount(1); + document.Meta["total-resources"].Should().Be(documentMetaValue); + document.Links.Self.Should().Be(topLevelLink); + document.Links.First.Should().Be(topLevelLink); + document.Links.Last.Should().Be(topLevelLink); + document.Data.Should().HaveCount(1); + + FlightDataInResponse flight = document.Data.First(); + flight.Id.Should().Be(flightId); + flight.Type.Should().Be(FlightsResourceType.Flights); + flight.Links.Self.Should().Be(flightResourceLink); + flight.Meta.Should().HaveCount(1); + flight.Meta["docs"].Should().Be(flightMetaValue); + + flight.Attributes.Destination.Should().Be(flightDestination); + flight.Attributes.FlightNumber.Should().Be(null); + flight.Attributes.ServicesOnBoard.Should().HaveCount(3); + flight.Attributes.ServicesOnBoard.ElementAt(0).Should().Be(fightServiceOnBoard); + flight.Attributes.ServicesOnBoard.ElementAt(1).Should().Be(string.Empty); + flight.Attributes.ServicesOnBoard.ElementAt(2).Should().BeNull(); + flight.Attributes.OperatedBy.Should().Be(Airline.DeltaAirLines); + flight.Attributes.DepartsAt.Should().Be(DateTimeOffset.Parse(flightDepartsAt, new CultureInfo("en-GB"))); + flight.Attributes.ArrivesAt.Should().Be(null); + + flight.Relationships.OperatingAirplane.Data.Should().BeNull(); + flight.Relationships.OperatingAirplane.Links.Self.Should().Be(flightResourceLink + "/relationships/operating-airplane"); + flight.Relationships.OperatingAirplane.Links.Related.Should().Be(flightResourceLink + "/operating-airplane"); + flight.Relationships.OperatingAirplane.Meta.Should().HaveCount(1); + flight.Relationships.OperatingAirplane.Meta["docs"].Should().Be(operatingAirplaneMetaValue); + + flight.Relationships.FlightAttendants.Data.Should().BeNull(); + flight.Relationships.FlightAttendants.Links.Self.Should().Be(flightResourceLink + "/relationships/flight-attendants"); + flight.Relationships.FlightAttendants.Links.Related.Should().Be(flightResourceLink + "/flight-attendants"); + flight.Relationships.FlightAttendants.Meta.Should().HaveCount(1); + flight.Relationships.FlightAttendants.Meta["docs"].Should().Be(flightAttendantsMetaValue); + + flight.Relationships.ReserveFlightAttendants.Data.Should().BeNull(); + flight.Relationships.ReserveFlightAttendants.Links.Self.Should().Be(flightResourceLink + "/relationships/reserve-flight-attendants"); + flight.Relationships.ReserveFlightAttendants.Links.Related.Should().Be(flightResourceLink + "/reserve-flight-attendants"); + flight.Relationships.ReserveFlightAttendants.Meta.Should().HaveCount(1); + flight.Relationships.ReserveFlightAttendants.Meta["docs"].Should().Be(reserveFlightAttendantsMetaValue); + } + + [Fact] + public async Task Getting_resource_translates_response() + { + // Arrange + const string flightId = "8712"; + const string departsAtInZuluTime = "2021-06-08T12:53:30.554Z"; + const string arrivesAtWithUtcOffset = "2019-02-20T11:56:33.0721266+01:00"; + + const string responseBody = @"{ + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]=departs-at,arrives-at"" + }, + ""data"": { + ""type"": ""flights"", + ""id"": """ + flightId + @""", + ""attributes"": { + ""departs-at"": """ + departsAtInZuluTime + @""", + ""arrives-at"": """ + arrivesAtWithUtcOffset + @""" + }, + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @""" + } + } +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + FlightPrimaryResponseDocument document = await apiClient.GetFlightAsync(Convert.ToInt32(flightId)); + + // Assert + document.Jsonapi.Should().BeNull(); + document.Meta.Should().BeNull(); + document.Data.Meta.Should().BeNull(); + 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.FlightNumber.Should().BeNull(); + document.Data.Attributes.ServicesOnBoard.Should().BeNull(); + document.Data.Attributes.Destination.Should().BeNull(); + document.Data.Attributes.OperatedBy.Should().Be(default(Airline)); + } + + [Fact] + public async Task Getting_unknown_resource_translates_error_response() + { + // Arrange + const string flightId = "8712"; + + const string responseBody = @"{ + ""errors"": [ + { + ""id"": ""f1a520ac-02a0-466b-94ea-86cbaa86f02f"", + ""status"": ""404"", + ""destination"": ""The requested resource does not exist."", + ""detail"": ""Resource of type 'meetings' with ID '" + flightId + @"' does not exist."" + } + ] +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NotFound, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + Func> action = async () => await apiClient.GetFlightAsync(Convert.ToInt32(flightId)); + + // Assert + ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); + ApiException exception = assertion.Subject.Single(); + + exception.StatusCode.Should().Be((int)HttpStatusCode.NotFound); + exception.Response.Should().Be(responseBody); + } + + [Fact] + public async Task Posting_resource_translates_response() + { + // Arrange + const string flightId = "8712"; + const string flightAttendantId = "bBJHu"; + + const string responseBody = @"{ + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,flight-attendants,reserve-flight-attendants"" + }, + ""data"": { + ""type"": ""flights"", + ""id"": """ + flightId + @""", + ""relationships"": { + ""operating-airplane"": { + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/operating-airplane"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/operating-airplane"" + }, + ""data"": null + }, + ""flight-attendants"": { + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/flight-attendants"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/flight-attendants"" + }, + ""data"": [ + { + ""type"": ""flight-attendants"", + ""id"": """ + flightAttendantId + @""" + } + ], + }, + ""reserve-flight-attendants"": { + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/reserve-flight-attendants"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/reserve-flight-attendants"" + }, + ""data"": [ ] + } + }, + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,flight-attendants,reserve-flight-attendants"" + } + } +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.Created, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + FlightPrimaryResponseDocument document = await apiClient.PostFlightAsync(new FlightPostRequestDocument + { + Data = new FlightDataInPostRequest + { + Type = FlightsResourceType.Flights, + Relationships = new FlightRelationshipsInPostRequest + { + OperatingAirplane = new ToOneAirplaneRequestData() + { + Data = new AirplaneIdentifier + { + Id = "XxuIu", + Type = AirplanesResourceType.Airplanes + } + } + } + } + }); + + // Assert + document.Data.Attributes.Should().BeNull(); + document.Data.Relationships.OperatingAirplane.Data.Should().BeNull(); + document.Data.Relationships.FlightAttendants.Data.Should().HaveCount(1); + document.Data.Relationships.FlightAttendants.Data.First().Id.Should().Be(flightAttendantId); + document.Data.Relationships.FlightAttendants.Data.First().Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); + document.Data.Relationships.ReserveFlightAttendants.Data.Should().BeEmpty(); + } + + [Fact] + public async Task Patching_resource_with_side_effects_translates_response() + { + // Arrange + const string flightId = "8712"; + + const string responseBody = @"{ + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]"" + }, + ""data"": { + ""type"": ""flights"", + ""id"": """ + flightId + @""", + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,flight-attendants,reserve-flight-attendants"" + } + } +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + FlightPrimaryResponseDocument document = await apiClient.PatchFlightAsync(Convert.ToInt32(flightId), new FlightPatchRequestDocument + { + Data = new FlightDataInPatchRequest + { + Id = flightId, + Type = FlightsResourceType.Flights + } + }); + + // Assert + document.Data.Type.Should().Be(FlightsResourceType.Flights); + document.Data.Attributes.Should().BeNull(); + document.Data.Relationships.Should().BeNull(); + } + + [Fact] + public async Task Patching_resource_without_side_effects_translates_response() + { + // Arrange + const string flightId = "8712"; + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await apiClient.PatchFlightAsync(Convert.ToInt32(flightId), + new FlightPatchRequestDocument + { + Data = new FlightDataInPatchRequest + { + Id = flightId, + Type = FlightsResourceType.Flights + } + })); + + // Assert + document.Should().BeNull(); + } + + [Fact] + public async Task Deleting_resource_produces_empty_response() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + Func action = async () => await apiClient.DeleteFlightAsync(8712); + + // Assert + await action.Should().NotThrowAsync(); + } + + [Fact] + public async Task Getting_secondary_resource_translates_response() + { + // Arrange + const string flightId = "8712"; + + const string responseBody = @"{ + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/operating-airplane"", + ""first"": """ + HostPrefix + @"flights/" + flightId + @"/operating-airplane"", + ""last"": """ + HostPrefix + @"flights/" + flightId + @"/operating-airplane"" + }, + ""data"": null +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + AirplaneSecondaryResponseDocument document = await apiClient.GetFlightOperatingAirplaneAsync(Convert.ToInt32(flightId)); + + // Assert + document.Data.Should().BeNull(); + } + + [Fact] + public async Task Getting_secondary_resources_translates_response() + { + // Arrange + const string flightId = "8712"; + + const string responseBody = @"{ + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/flight-attendants"", + ""first"": """ + HostPrefix + @"flights/" + flightId + @"/flight-attendants"" + }, + ""data"": [ ] +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + FlightAttendantCollectionResponseDocument document = await apiClient.GetFlightFlightAttendantsAsync(Convert.ToInt32(flightId)); + + // Assert + document.Data.Should().BeEmpty(); + } + + [Fact] + public async Task Getting_ToOne_relationship_translates_response() + { + // Arrange + const string flightId = "8712"; + const string operatingAirplaneId = "bBJHu"; + + const string responseBody = @"{ + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/operating-airplane"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/operating-airplane"" + }, + ""data"": { + ""type"": ""airplanes"", + ""id"": """ + operatingAirplaneId + @""" + } +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + AirplaneIdentifierResponseDocument document = await apiClient.GetFlightOperatingAirplaneRelationshipAsync(Convert.ToInt32(flightId)); + + // Assert + document.Data.Should().NotBeNull(); + document.Data.Id.Should().Be(operatingAirplaneId); + document.Data.Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); + } + + [Fact] + public async Task Patching_ToOne_relationship_translates_response() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + await apiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData() + { + Data = new AirplaneIdentifier() + { + Id = "Adk2a", + Type = AirplanesResourceType.Airplanes + } + }); + } + + [Fact] + public async Task Getting_ToMany_relationship_translates_response() + { + // Arrange + const string flightId = "8712"; + const string flightAttendantId1 = "bBJHu"; + const string flightAttendantId2 = "ZvuHNInmX1"; + + const string responseBody = @"{ + ""links"": { + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/flight-attendants"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/flight-attendants"", + ""first"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/flight-attendants"" + }, + ""data"": [{ + ""type"": ""flight-attendants"", + ""id"": """ + flightAttendantId1 + @""" + }, + { + ""type"": ""flight-attendants"", + ""id"": """ + flightAttendantId2 + @""" + }] +}"; + + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + FlightAttendantIdentifierCollectionResponseDocument document = await apiClient.GetFlightFlightAttendantsRelationshipAsync(Convert.ToInt32(flightId)); + + // Assert + document.Data.Should().HaveCount(2); + document.Data.First().Id.Should().Be(flightAttendantId1); + document.Data.First().Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); + document.Data.Last().Id.Should().Be(flightAttendantId2); + document.Data.Last().Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); + } + + [Fact] + public async Task Posting_ToMany_relationship_produces_empty_response() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + Func action = async () => await apiClient.PostFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData() + { + Data = new List + { + new() + { + Id = "Adk2a", + Type = FlightAttendantsResourceType.FlightAttendants + }, + new() + { + Id = "Un37k", + Type = FlightAttendantsResourceType.FlightAttendants + } + } + }); + + // Assert + await action.Should().NotThrowAsync(); + } + + [Fact] + public async Task Patching_ToMany_relationship_produces_empty_response() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + Func action = async () => await apiClient.PatchFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData() + { + Data = new List + { + new() + { + Id = "Adk2a", + Type = FlightAttendantsResourceType.FlightAttendants + }, + new() + { + Id = "Un37k", + Type = FlightAttendantsResourceType.FlightAttendants + } + } + }); + + // Assert + await action.Should().NotThrowAsync(); + } + + [Fact] + public async Task Deleting_ToMany_relationship_produces_empty_response() + { + // Arrange + using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + + // Act + Func action = async () => await apiClient.DeleteFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData() + { + Data = new List + { + new() + { + Id = "Adk2a", + Type = FlightAttendantsResourceType.FlightAttendants + }, + new() + { + Id = "Un37k", + Type = FlightAttendantsResourceType.FlightAttendants + } + } + }); + + // Assert + await action.Should().NotThrowAsync(); + } + } +} diff --git a/test/OpenApiTests/Flight.cs b/test/OpenApiTests/Flight.cs index bddd4c1d99..1f185c8c1f 100644 --- a/test/OpenApiTests/Flight.cs +++ b/test/OpenApiTests/Flight.cs @@ -1,27 +1,63 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; +// +// +// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] +// [Required, MaxLength(255)] +// public string Title { get; set; } +// +// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] +// [MaxLength(2000)] +// public string Summary { get; set; } +// +// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] +// public int? DurationInSeconds { get; set; } +// +// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] +// public DateTime? PublishedAt { get; set; } +// +// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] +// public bool IsDeleted { get; set; } +// +// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] +// [MaxLength(2000)] +// public string ExternalId { get; set; } + + namespace OpenApiTests { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Flight : Identifiable { - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + [Required] + [MaxLength(40)] public string Destination { get; set; } - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] - public DateTimeOffset DepartsAt { get; set; } + [Attr] + public DateTime? DepartsAt { get; set; } + + [Attr] + public DateTime? ArrivesAt { get; set; } + + [Attr(PublicName = "operated-by", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + public Airline Airline { get; set; } + + [Attr] + public ICollection ServicesOnBoard { get; set; } [HasOne] - public Airplane ServicingAirplane { get; set; } + public Airplane OperatingAirplane { get; set; } [HasMany(PublicName = "flight-attendants")] public ISet CabinPersonnel { get; set; } - [HasOne] - public FlightAttendant Purser { get; set; } + [HasMany(PublicName = "reserve-flight-attendants")] + public ICollection BackupPersonnel { get; set; } } } diff --git a/test/OpenApiTests/FlightAttendant.cs b/test/OpenApiTests/FlightAttendant.cs index 5b04b35450..625a26516b 100644 --- a/test/OpenApiTests/FlightAttendant.cs +++ b/test/OpenApiTests/FlightAttendant.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; @@ -7,10 +8,10 @@ namespace OpenApiTests { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class FlightAttendant : Identifiable + public sealed class FlightAttendant : Identifiable { [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowFilter)] - public override long Id { get; set; } + public override string Id { get; set; } [Attr(Capabilities = AttrCapabilities.None)] public FlightAttendantExpertise ExpertiseLevel { get; set; } @@ -27,16 +28,18 @@ public sealed class FlightAttendant : Identifiable [Attr(Capabilities = AttrCapabilities.All)] [Required] - [Url] - public string ProfileImageUrl { get; set; } + [Range(18, 75)] + public int Age { get; set; } [Attr(Capabilities = AttrCapabilities.All)] - public ICollection DestinationPreferences { get; set; } + [Required] + [Url] + public string ProfileImageUrl { get; set; } [HasMany] - public ISet Flights { get; set; } + public ISet ScheduledForFlights { get; set; } - [HasOne] - public Flight PurserOnFlight { get; set; } + [HasMany] + public ISet StandbyForFlights { get; set; } } } diff --git a/test/OpenApiTests/FlightAttendantExpertise.cs b/test/OpenApiTests/FlightAttendantExpertise.cs index f13544d23b..d5f647d3b8 100644 --- a/test/OpenApiTests/FlightAttendantExpertise.cs +++ b/test/OpenApiTests/FlightAttendantExpertise.cs @@ -1,5 +1,8 @@ +using JetBrains.Annotations; + namespace OpenApiTests { + [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum FlightAttendantExpertise { Junior, diff --git a/test/OpenApiTests/FlightAttendantsController.cs b/test/OpenApiTests/FlightAttendantsController.cs index a19f267c72..aed6adea28 100644 --- a/test/OpenApiTests/FlightAttendantsController.cs +++ b/test/OpenApiTests/FlightAttendantsController.cs @@ -5,9 +5,9 @@ namespace OpenApiTests { - public sealed class FlightAttendantsController : JsonApiController + public sealed class FlightAttendantsController : JsonApiController { - public FlightAttendantsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + public FlightAttendantsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/OpenApiTests/OpenApiDbContext.cs b/test/OpenApiTests/OpenApiDbContext.cs index ea3571b3ff..175450d23c 100644 --- a/test/OpenApiTests/OpenApiDbContext.cs +++ b/test/OpenApiTests/OpenApiDbContext.cs @@ -22,15 +22,14 @@ protected override void OnModelCreating(ModelBuilder builder) { builder.Entity() .HasMany(flight => flight.CabinPersonnel) - .WithMany(flightAttendant => flightAttendant.Flights); + .WithMany(flightAttendant => flightAttendant.ScheduledForFlights); - builder.Entity() - .Ignore(flightAttendant => flightAttendant.DestinationPreferences); + builder.Entity() + .HasMany(flight => flight.BackupPersonnel) + .WithMany(flightAttendant => flightAttendant.StandbyForFlights); builder.Entity() - .HasOne(flight => flight.Purser) - .WithOne(flightAttendant => flightAttendant.PurserOnFlight) - .HasForeignKey("PurserId"); + .Ignore(flight => flight.ServicesOnBoard); } } } diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index c4d01aadcf..366fecc5e5 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -4,6 +4,7 @@ + @@ -12,8 +13,30 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + OpenApiTests.ClientLibrary.GeneratedCode + OpenApiClient + NSwagCSharp + /UseBaseUrl:false /GenerateClientInterfaces:true + + + diff --git a/test/OpenApiTests/swagger.json b/test/OpenApiTests/swagger.json index c2e54f5a45..c1306eebbd 100644 --- a/test/OpenApiTests/swagger.json +++ b/test/OpenApiTests/swagger.json @@ -1,3 +1,4 @@ + { "openapi": "3.0.1", "info": { @@ -478,8 +479,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -507,8 +507,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -536,8 +535,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -577,8 +575,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -589,20 +586,19 @@ } } }, - "/api/v1/flight-attendants/{id}/flights": { + "/api/v1/flight-attendants/{id}/scheduled-for-flights": { "get": { "tags": [ "flight-attendants" ], - "operationId": "get-flight-attendant-flights", + "operationId": "get-flight-attendant-scheduled-for-flights", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -623,15 +619,14 @@ "tags": [ "flight-attendants" ], - "operationId": "head-flight-attendant-flights", + "operationId": "head-flight-attendant-scheduled-for-flights", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -649,20 +644,19 @@ } } }, - "/api/v1/flight-attendants/{id}/relationships/flights": { + "/api/v1/flight-attendants/{id}/relationships/scheduled-for-flights": { "get": { "tags": [ "flight-attendants" ], - "operationId": "get-flight-attendant-flights-relationship", + "operationId": "get-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -683,15 +677,14 @@ "tags": [ "flight-attendants" ], - "operationId": "head-flight-attendant-flights-relationship", + "operationId": "head-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -712,15 +705,14 @@ "tags": [ "flight-attendants" ], - "operationId": "post-flight-attendant-flights-relationship", + "operationId": "post-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -743,15 +735,14 @@ "tags": [ "flight-attendants" ], - "operationId": "patch-flight-attendant-flights-relationship", + "operationId": "patch-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -774,15 +765,14 @@ "tags": [ "flight-attendants" ], - "operationId": "delete-flight-attendant-flights-relationship", + "operationId": "delete-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -802,20 +792,19 @@ } } }, - "/api/v1/flight-attendants/{id}/purser-on-flight": { + "/api/v1/flight-attendants/{id}/standby-for-flights": { "get": { "tags": [ "flight-attendants" ], - "operationId": "get-flight-attendant-purser-on-flight", + "operationId": "get-flight-attendant-standby-for-flights", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -825,7 +814,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-secondary-response-document" + "$ref": "#/components/schemas/flight-collection-response-document" } } } @@ -836,15 +825,14 @@ "tags": [ "flight-attendants" ], - "operationId": "head-flight-attendant-purser-on-flight", + "operationId": "head-flight-attendant-standby-for-flights", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -854,7 +842,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-secondary-response-document" + "$ref": "#/components/schemas/flight-collection-response-document" } } } @@ -862,20 +850,19 @@ } } }, - "/api/v1/flight-attendants/{id}/relationships/purser-on-flight": { + "/api/v1/flight-attendants/{id}/relationships/standby-for-flights": { "get": { "tags": [ "flight-attendants" ], - "operationId": "get-flight-attendant-purser-on-flight-relationship", + "operationId": "get-flight-attendant-standby-for-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -885,7 +872,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-identifier-response-document" + "$ref": "#/components/schemas/flight-identifier-collection-response-document" } } } @@ -896,15 +883,14 @@ "tags": [ "flight-attendants" ], - "operationId": "head-flight-attendant-purser-on-flight-relationship", + "operationId": "head-flight-attendant-standby-for-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -914,26 +900,85 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-identifier-response-document" + "$ref": "#/components/schemas/flight-identifier-collection-response-document" } } } } } }, + "post": { + "tags": [ + "flight-attendants" + ], + "operationId": "post-flight-attendant-standby-for-flights-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, "patch": { "tags": [ "flight-attendants" ], - "operationId": "patch-flight-attendant-purser-on-flight-relationship", + "operationId": "patch-flight-attendant-standby-for-flights-relationship", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "flight-attendants" + ], + "operationId": "delete-flight-attendant-standby-for-flights-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" } } ], @@ -941,7 +986,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-flight-request-data" + "$ref": "#/components/schemas/to-many-flight-request-data" } } } @@ -1357,12 +1402,12 @@ } } }, - "/api/v1/flights/{id}/purser": { + "/api/v1/flights/{id}/operating-airplane": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-purser", + "operationId": "get-flight-operating-airplane", "parameters": [ { "name": "id", @@ -1380,7 +1425,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-secondary-response-document" + "$ref": "#/components/schemas/airplane-secondary-response-document" } } } @@ -1391,7 +1436,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-purser", + "operationId": "head-flight-operating-airplane", "parameters": [ { "name": "id", @@ -1409,7 +1454,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-secondary-response-document" + "$ref": "#/components/schemas/airplane-secondary-response-document" } } } @@ -1417,12 +1462,12 @@ } } }, - "/api/v1/flights/{id}/relationships/purser": { + "/api/v1/flights/{id}/relationships/operating-airplane": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-purser-relationship", + "operationId": "get-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1440,7 +1485,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-identifier-response-document" + "$ref": "#/components/schemas/airplane-identifier-response-document" } } } @@ -1451,7 +1496,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-purser-relationship", + "operationId": "head-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1469,7 +1514,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-identifier-response-document" + "$ref": "#/components/schemas/airplane-identifier-response-document" } } } @@ -1480,7 +1525,7 @@ "tags": [ "flights" ], - "operationId": "patch-flight-purser-relationship", + "operationId": "patch-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1496,7 +1541,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-flight-attendant-request-data" + "$ref": "#/components/schemas/to-one-airplane-request-data" } } } @@ -1508,12 +1553,12 @@ } } }, - "/api/v1/flights/{id}/servicing-airplane": { + "/api/v1/flights/{id}/reserve-flight-attendants": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-servicing-airplane", + "operationId": "get-flight-reserve-flight-attendants", "parameters": [ { "name": "id", @@ -1531,7 +1576,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-secondary-response-document" + "$ref": "#/components/schemas/flight-attendant-collection-response-document" } } } @@ -1542,7 +1587,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-servicing-airplane", + "operationId": "head-flight-reserve-flight-attendants", "parameters": [ { "name": "id", @@ -1560,7 +1605,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-secondary-response-document" + "$ref": "#/components/schemas/flight-attendant-collection-response-document" } } } @@ -1568,12 +1613,12 @@ } } }, - "/api/v1/flights/{id}/relationships/servicing-airplane": { + "/api/v1/flights/{id}/relationships/reserve-flight-attendants": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-servicing-airplane-relationship", + "operationId": "get-flight-reserve-flight-attendants-relationship", "parameters": [ { "name": "id", @@ -1591,7 +1636,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-identifier-response-document" + "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" } } } @@ -1602,7 +1647,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-servicing-airplane-relationship", + "operationId": "head-flight-reserve-flight-attendants-relationship", "parameters": [ { "name": "id", @@ -1620,18 +1665,49 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-identifier-response-document" + "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" } } } } } }, + "post": { + "tags": [ + "flights" + ], + "operationId": "post-flight-reserve-flight-attendants-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, "patch": { "tags": [ "flights" ], - "operationId": "patch-flight-servicing-airplane-relationship", + "operationId": "patch-flight-reserve-flight-attendants-relationship", "parameters": [ { "name": "id", @@ -1647,7 +1723,38 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-airplane-request-data" + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "flights" + ], + "operationId": "delete-flight-reserve-flight-attendants-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" } } } @@ -1662,12 +1769,119 @@ }, "components": { "schemas": { + "aircraft-type": { + "enum": [ + "Turboprops", + "LightJet", + "MidSizeJet", + "JumboJet" + ], + "type": "string" + }, + "airline": { + "enum": [ + "DeltaAirLines", + "LufthansaGroup", + "AirFranceKlm" + ], + "type": "string" + }, + "airplane-attributes-in-patch-request": { + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "type": "string" + }, + "manufactured-at": { + "type": "string", + "format": "date-time" + }, + "airtime-in-hours": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "last-serviced-at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "is-in-maintenance": { + "type": "boolean" + }, + "serial-number": { + "maxLength": 2000, + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "airplane-attributes-in-post-request": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 255, + "type": "string" + }, + "manufactured-at": { + "type": "string", + "format": "date-time" + }, + "airtime-in-hours": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "seating-capacity": { + "type": "integer", + "format": "int32" + }, + "airplane-type": { + "$ref": "#/components/schemas/aircraft-type" + } + }, + "additionalProperties": false + }, "airplane-attributes-in-response": { "type": "object", "properties": { - "id": { + "name": { + "maxLength": 255, + "type": "string" + }, + "manufactured-at": { + "type": "string", + "format": "date-time" + }, + "airtime-in-hours": { + "type": "integer", + "format": "int32", + "nullable": true + }, + "last-serviced-at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "is-in-maintenance": { + "type": "boolean" + }, + "serial-number": { + "maxLength": 2000, "type": "string", "nullable": true + }, + "seating-capacity": { + "type": "integer", + "format": "int32" + }, + "airplane-type": { + "$ref": "#/components/schemas/aircraft-type" } }, "additionalProperties": false @@ -1710,6 +1924,12 @@ }, "id": { "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/airplane-attributes-in-patch-request" + }, + "relationships": { + "$ref": "#/components/schemas/airplane-relationships-in-patch-request" } }, "additionalProperties": false @@ -1722,6 +1942,12 @@ "properties": { "type": { "$ref": "#/components/schemas/airplanes-resource-type" + }, + "attributes": { + "$ref": "#/components/schemas/airplane-attributes-in-post-request" + }, + "relationships": { + "$ref": "#/components/schemas/airplane-relationships-in-post-request" } }, "additionalProperties": false @@ -1743,6 +1969,9 @@ "attributes": { "$ref": "#/components/schemas/airplane-attributes-in-response" }, + "relationships": { + "$ref": "#/components/schemas/airplane-relationships-in-response" + }, "links": { "$ref": "#/components/schemas/links-in-resource-object" }, @@ -1761,7 +1990,7 @@ "type": "object", "properties": { "type": { - "type": "string" + "$ref": "#/components/schemas/airplanes-resource-type" }, "id": { "type": "string" @@ -1846,24 +2075,51 @@ }, "additionalProperties": false }, - "airplane-secondary-response-document": { - "required": [ - "data", - "links" - ], + "airplane-relationships-in-patch-request": { "type": "object", "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-document" - }, - "data": { + "flights": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + }, + "additionalProperties": false + }, + "airplane-relationships-in-post-request": { + "type": "object", + "properties": { + "flights": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + }, + "additionalProperties": false + }, + "airplane-relationships-in-response": { + "type": "object", + "properties": { + "flights": { + "$ref": "#/components/schemas/to-many-flight-response-data" + } + }, + "additionalProperties": false + }, + "airplane-secondary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + }, + "data": { "oneOf": [ { "$ref": "#/components/schemas/airplane-data-in-response" @@ -1882,12 +2138,76 @@ ], "type": "string" }, + "flight-attendant-attributes-in-patch-request": { + "type": "object", + "properties": { + "document-number": { + "maxLength": 9, + "type": "string" + }, + "email-address": { + "type": "string", + "format": "email" + }, + "age": { + "maximum": 75, + "minimum": 18, + "type": "integer", + "format": "int32" + }, + "profile-image-url": { + "type": "string", + "format": "uri" + } + }, + "additionalProperties": false + }, + "flight-attendant-attributes-in-post-request": { + "required": [ + "document-number", + "email-address", + "age", + "profile-image-url" + ], + "type": "object", + "properties": { + "document-number": { + "maxLength": 9, + "type": "string" + }, + "email-address": { + "type": "string", + "format": "email" + }, + "age": { + "maximum": 75, + "minimum": 18, + "type": "integer", + "format": "int32" + }, + "profile-image-url": { + "type": "string", + "format": "uri" + } + }, + "additionalProperties": false + }, "flight-attendant-attributes-in-response": { "type": "object", "properties": { - "id": { + "email-address": { + "type": "string", + "format": "email" + }, + "age": { + "maximum": 75, + "minimum": 18, "type": "integer", - "format": "int64" + "format": "int32" + }, + "profile-image-url": { + "type": "string", + "format": "uri" } }, "additionalProperties": false @@ -1930,6 +2250,12 @@ }, "id": { "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/flight-attendant-attributes-in-patch-request" + }, + "relationships": { + "$ref": "#/components/schemas/flight-attendant-relationships-in-patch-request" } }, "additionalProperties": false @@ -1942,6 +2268,12 @@ "properties": { "type": { "$ref": "#/components/schemas/flight-attendants-resource-type" + }, + "attributes": { + "$ref": "#/components/schemas/flight-attendant-attributes-in-post-request" + }, + "relationships": { + "$ref": "#/components/schemas/flight-attendant-relationships-in-post-request" } }, "additionalProperties": false @@ -1963,6 +2295,9 @@ "attributes": { "$ref": "#/components/schemas/flight-attendant-attributes-in-response" }, + "relationships": { + "$ref": "#/components/schemas/flight-attendant-relationships-in-response" + }, "links": { "$ref": "#/components/schemas/links-in-resource-object" }, @@ -1981,7 +2316,7 @@ "type": "object", "properties": { "type": { - "type": "string" + "$ref": "#/components/schemas/flight-attendants-resource-type" }, "id": { "type": "string" @@ -2015,36 +2350,6 @@ }, "additionalProperties": false }, - "flight-attendant-identifier-response-document": { - "required": [ - "data", - "links" - ], - "type": "object", - "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" - }, - "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/flight-attendant-identifier" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] - } - }, - "additionalProperties": false - }, "flight-attendant-patch-request-document": { "required": [ "data" @@ -2092,32 +2397,38 @@ }, "additionalProperties": false }, - "flight-attendant-secondary-response-document": { - "required": [ - "data", - "links" - ], + "flight-attendant-relationships-in-patch-request": { "type": "object", "properties": { - "meta": { - "type": "object", - "additionalProperties": {} + "scheduled-for-flights": { + "$ref": "#/components/schemas/to-many-flight-request-data" }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "standby-for-flights": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + }, + "additionalProperties": false + }, + "flight-attendant-relationships-in-post-request": { + "type": "object", + "properties": { + "scheduled-for-flights": { + "$ref": "#/components/schemas/to-many-flight-request-data" }, - "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "standby-for-flights": { + "$ref": "#/components/schemas/to-many-flight-request-data" + } + }, + "additionalProperties": false + }, + "flight-attendant-relationships-in-response": { + "type": "object", + "properties": { + "scheduled-for-flights": { + "$ref": "#/components/schemas/to-many-flight-response-data" }, - "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/flight-attendant-data-in-response" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] + "standby-for-flights": { + "$ref": "#/components/schemas/to-many-flight-response-data" } }, "additionalProperties": false @@ -2128,12 +2439,55 @@ ], "type": "string" }, + "flight-attributes-in-patch-request": { + "type": "object", + "properties": { + "destination": { + "maxLength": 40, + "type": "string" + }, + "flight-number": { + "maxLength": 8, + "type": "string", + "nullable": true + }, + "operated-by": { + "$ref": "#/components/schemas/airline" + } + }, + "additionalProperties": false + }, "flight-attributes-in-response": { "type": "object", "properties": { - "id": { - "type": "integer", - "format": "int32" + "destination": { + "maxLength": 40, + "type": "string" + }, + "flight-number": { + "maxLength": 8, + "type": "string", + "nullable": true + }, + "departs-at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "arrives-at": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "operated-by": { + "$ref": "#/components/schemas/airline" + }, + "services-on-board": { + "type": "array", + "items": { + "type": "string" + }, + "nullable": true } }, "additionalProperties": false @@ -2176,6 +2530,12 @@ }, "id": { "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/flight-attributes-in-patch-request" + }, + "relationships": { + "$ref": "#/components/schemas/flight-relationships-in-patch-request" } }, "additionalProperties": false @@ -2188,6 +2548,9 @@ "properties": { "type": { "$ref": "#/components/schemas/flights-resource-type" + }, + "relationships": { + "$ref": "#/components/schemas/flight-relationships-in-post-request" } }, "additionalProperties": false @@ -2209,6 +2572,9 @@ "attributes": { "$ref": "#/components/schemas/flight-attributes-in-response" }, + "relationships": { + "$ref": "#/components/schemas/flight-relationships-in-response" + }, "links": { "$ref": "#/components/schemas/links-in-resource-object" }, @@ -2227,7 +2593,7 @@ "type": "object", "properties": { "type": { - "type": "string" + "$ref": "#/components/schemas/flights-resource-type" }, "id": { "type": "string" @@ -2261,36 +2627,6 @@ }, "additionalProperties": false }, - "flight-identifier-response-document": { - "required": [ - "data", - "links" - ], - "type": "object", - "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" - }, - "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/flight-identifier" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] - } - }, - "additionalProperties": false - }, "flight-patch-request-document": { "required": [ "data" @@ -2338,32 +2674,47 @@ }, "additionalProperties": false }, - "flight-secondary-response-document": { - "required": [ - "data", - "links" - ], + "flight-relationships-in-patch-request": { "type": "object", "properties": { - "meta": { - "type": "object", - "additionalProperties": {} + "operating-airplane": { + "$ref": "#/components/schemas/to-one-airplane-request-data" }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" + "flight-attendants": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" }, - "links": { - "$ref": "#/components/schemas/links-in-resource-document" + "reserve-flight-attendants": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + } + }, + "additionalProperties": false + }, + "flight-relationships-in-post-request": { + "type": "object", + "properties": { + "operating-airplane": { + "$ref": "#/components/schemas/to-one-airplane-request-data" }, - "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/flight-data-in-response" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] + "flight-attendants": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + }, + "reserve-flight-attendants": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + } + }, + "additionalProperties": false + }, + "flight-relationships-in-response": { + "type": "object", + "properties": { + "operating-airplane": { + "$ref": "#/components/schemas/to-one-airplane-response-data" + }, + "flight-attendants": { + "$ref": "#/components/schemas/to-many-flight-attendant-response-data" + }, + "reserve-flight-attendants": { + "$ref": "#/components/schemas/to-many-flight-attendant-response-data" } }, "additionalProperties": false @@ -2399,6 +2750,22 @@ }, "additionalProperties": false }, + "links-in-relationship-object": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "related": { + "type": "string" + } + }, + "additionalProperties": false + }, "links-in-resource-collection-document": { "required": [ "first", @@ -2543,6 +2910,28 @@ }, "additionalProperties": false }, + "to-many-flight-attendant-response-data": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-attendant-identifier" + } + } + }, + "additionalProperties": false + }, "to-many-flight-request-data": { "required": [ "data" @@ -2558,26 +2947,29 @@ }, "additionalProperties": false }, - "to-one-airplane-request-data": { + "to-many-flight-response-data": { "required": [ - "data" + "links" ], "type": "object", "properties": { + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/airplane-identifier" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/flight-identifier" + } } }, "additionalProperties": false }, - "to-one-flight-attendant-request-data": { + "to-one-airplane-request-data": { "required": [ "data" ], @@ -2586,7 +2978,7 @@ "data": { "oneOf": [ { - "$ref": "#/components/schemas/flight-attendant-identifier" + "$ref": "#/components/schemas/airplane-identifier" }, { "$ref": "#/components/schemas/null-value" @@ -2596,16 +2988,23 @@ }, "additionalProperties": false }, - "to-one-flight-request-data": { + "to-one-airplane-response-data": { "required": [ - "data" + "links" ], "type": "object", "properties": { + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, "data": { "oneOf": [ { - "$ref": "#/components/schemas/flight-identifier" + "$ref": "#/components/schemas/airplane-identifier" }, { "$ref": "#/components/schemas/null-value" @@ -2617,4 +3016,4 @@ } } } -} +} \ No newline at end of file diff --git a/test/TestBuildingBlocks/FakeHttpClientWrapper.cs b/test/TestBuildingBlocks/FakeHttpClientWrapper.cs new file mode 100644 index 0000000000..537cea9d19 --- /dev/null +++ b/test/TestBuildingBlocks/FakeHttpClientWrapper.cs @@ -0,0 +1,107 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using JsonApiDotNetCore; + +namespace TestBuildingBlocks + +{ + /// + /// Enables to inject an outgoing response body and inspect the incoming request. + /// + public sealed class FakeHttpClientWrapper : IDisposable + { + private readonly FakeHttpMessageHandler _handler; + + public HttpClient HttpClient { get; } + public HttpRequestMessage Request => _handler.Request; + public string RequestBody => _handler.RequestBody; + + private FakeHttpClientWrapper(HttpClient httpClient, FakeHttpMessageHandler handler) + { + HttpClient = httpClient; + _handler = handler; + } + + public static FakeHttpClientWrapper Create(HttpStatusCode statusCode, string responseBody) + { + HttpResponseMessage response = CreateResponse(statusCode, responseBody); + var handler = new FakeHttpMessageHandler(); + handler.SetResponse(response); + + var httpClient = new HttpClient(handler) + { + BaseAddress = new Uri("http://localhost") + }; + + return new FakeHttpClientWrapper(httpClient, handler); + } + + public void ChangeResponse(HttpStatusCode statusCode, string responseBody) + { + HttpResponseMessage response = CreateResponse(statusCode, responseBody); + + _handler.SetResponse(response); + } + + private static HttpResponseMessage CreateResponse(HttpStatusCode statusCode, string responseBody) + { + var response = new HttpResponseMessage(statusCode); + + if (!string.IsNullOrEmpty(responseBody)) + { + response.Content = new StringContent(responseBody, Encoding.UTF8); + response.Content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/vnd.api+json"); + } + + return response; + } + + public void Dispose() + { + HttpClient.Dispose(); + _handler.Dispose(); + } + + private sealed class FakeHttpMessageHandler : HttpMessageHandler + { + private HttpResponseMessage _response; + + public HttpRequestMessage Request { get; private set; } + public string RequestBody { get; private set; } + + public void SetResponse(HttpResponseMessage response) + { + ArgumentGuard.NotNull(response, nameof(response)); + + _response = response; + } + + protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) + { + Request = request; + + // Capture the request body here, before it becomes inaccessible because the request has been disposed. + if (request.Content != null) + { + using Stream stream = request.Content.ReadAsStream(); + using var reader = new StreamReader(stream, Encoding.UTF8); + RequestBody = reader.ReadToEnd(); + } + + return _response; + } + + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpResponseMessage response = Send(request, cancellationToken); + return Task.FromResult(response); + } + } + } +} diff --git a/test/TestBuildingBlocks/HttpRequestHeadersExtensions.cs b/test/TestBuildingBlocks/HttpRequestHeadersExtensions.cs new file mode 100644 index 0000000000..f8cd609c82 --- /dev/null +++ b/test/TestBuildingBlocks/HttpRequestHeadersExtensions.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Headers; +using Microsoft.Extensions.Primitives; + +namespace TestBuildingBlocks +{ + public static class HttpRequestHeadersExtensions + { + /// + /// Returns the value of the specified HTTP request header, or null when not found. If the header occurs multiple times, their values are + /// collapsed into a comma-separated string, without changing any surrounding double quotes. + /// + public static string GetValue(this HttpRequestHeaders requestHeaders, string name) + { + bool headerExists = requestHeaders.TryGetValues(name, out IEnumerable values); + + return headerExists ? new StringValues(values.ToArray()).ToString() : null; + } + } +} From ec2ccafb1ab1eb2b7477455e2f4fa926dccf8695 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 10 Sep 2021 00:00:07 +0200 Subject: [PATCH 05/35] Adjusted build script to create OpenApiClient artifact, added documentation, minor model adjustments --- Build.ps1 | 2 + JsonApiDotNetCore.sln | 15 ---- docs/usage/{ => openapi}/openapi.md | 4 +- docs/usage/openapi/openapiclient.md | 79 +++++++++++++++++++ docs/usage/toc.md | 5 +- .../JsonApiDataContractResolver.cs | 15 ---- .../JsonApiDotNetCore.OpenApiClient.csproj | 2 +- test/OpenApiTests/Airplane.cs | 5 -- ...lientAttributeRegistrationLifeTimeTests.cs | 2 +- .../ClientLibrary/RequestTests.cs | 13 +-- .../ClientLibrary/ResponseTests.cs | 15 ++-- test/OpenApiTests/Flight.cs | 3 +- test/OpenApiTests/FlightAttendant.cs | 1 - 13 files changed, 105 insertions(+), 56 deletions(-) rename docs/usage/{ => openapi}/openapi.md (95%) create mode 100644 docs/usage/openapi/openapiclient.md diff --git a/Build.ps1 b/Build.ps1 index e3c05d0f89..29cbf44f17 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -86,10 +86,12 @@ function CreateNuGetPackage { if ([string]::IsNullOrWhitespace($versionSuffix)) { dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts dotnet pack .\src\JsonApiDotNetCore.OpenApi -c Release -o .\artifacts + dotnet pack .\src\JsonApiDotNetCore.OpenApiClient -c Release -o .\artifacts } else { dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts --version-suffix=$versionSuffix dotnet pack .\src\JsonApiDotNetCore.OpenApi -c Release -o .\artifacts --version-suffix=$versionSuffix + dotnet pack .\src\JsonApiDotNetCore.OpenApiClient -c Release -o .\artifacts --version-suffix=$versionSuffix } CheckLastExitCode diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln index 4adbd778fb..1b115959f6 100644 --- a/JsonApiDotNetCore.sln +++ b/JsonApiDotNetCore.sln @@ -48,8 +48,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiTests", "test\OpenApiTests\OpenApiTests.csproj", "{B693DE14-BB28-496F-AB39-B4E674ABCA80}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiClient", "src\Examples\OpenApiClient\OpenApiClient.csproj", "{63C2C6C1-0967-4439-BB63-A55DA22869AB}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApiClient", "src\JsonApiDotNetCore.OpenApiClient\JsonApiDotNetCore.OpenApiClient.csproj", "{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}" EndProject Global @@ -242,18 +240,6 @@ Global {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x64.Build.0 = Release|Any CPU {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.ActiveCfg = Release|Any CPU {B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.Build.0 = Release|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x64.ActiveCfg = Debug|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x64.Build.0 = Debug|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x86.ActiveCfg = Debug|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Debug|x86.Build.0 = Debug|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|Any CPU.Build.0 = Release|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x64.ActiveCfg = Release|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x64.Build.0 = Release|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x86.ActiveCfg = Release|Any CPU - {63C2C6C1-0967-4439-BB63-A55DA22869AB}.Release|x86.Build.0 = Release|Any CPU {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|Any CPU.Build.0 = Debug|Any CPU {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -286,7 +272,6 @@ Global {210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {71287D6F-6C3B-44B4-9FCA-E78FE3F02289} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} {B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} - {63C2C6C1-0967-4439-BB63-A55DA22869AB} = {026FBC6C-AF76-4568-9B87-EC73457899FD} {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/docs/usage/openapi.md b/docs/usage/openapi/openapi.md similarity index 95% rename from docs/usage/openapi.md rename to docs/usage/openapi/openapi.md index 79156c5de7..865f94d1bb 100644 --- a/docs/usage/openapi.md +++ b/docs/usage/openapi/openapi.md @@ -39,8 +39,8 @@ public class Startup IMvcCoreBuilder mvcBuilder = services.AddMvcCore(); services.AddJsonApi(mvcBuilder: mvcBuilder); - // Adds the Swashbuckle integration. - services.AddOpenApi(mvcBuilder); + // Adds the Swashbuckle integration. + services.AddOpenApi(mvcBuilder); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/docs/usage/openapi/openapiclient.md b/docs/usage/openapi/openapiclient.md new file mode 100644 index 0000000000..b61644f919 --- /dev/null +++ b/docs/usage/openapi/openapiclient.md @@ -0,0 +1,79 @@ +# OpenAPI Generated Client + +Given an OpenAPI specification, you can generate a client library using [NSwag](http://stevetalkscode.co.uk/openapireference-commands). We provide additional methods to enrich the features of this client. + +## Installation + +You need to install the following NuGet packages: + +- `JsonApiDotNetCore.OpenApiClient` +- `NSwag.ApiDescription.Client` +- `Microsoft.Extensions.ApiDescription.Cient` +- `NSwag.ApiDescription.Client` + +The following examples demonstrate how to install the `JsonApiDotNetCore.OpenApiClient` package. + +### CLI + +``` +dotnet add package JsonApiDotNetCore.OpenApiClient +``` + +### Visual Studio + +```powershell +Install-Package JsonApiDotNetCore.OpenApiClient +``` + +### *.csproj + +```xml + + + + +``` + + +## OpenApiReference + +Add a reference to your OpenAPI specification in your project file as demonstrated below. + +```xml + + + YourApplication.GeneratedCode + OpenApiClient + NSwagCSharp + /UseBaseUrl:false /GenerateClientInterfaces:true + + +``` + + +## Usage + +The NSwag tooling generates the OpenAPI client during a prebuild step. Once your application is built, +you can instantiate it using the class name as indicated in the project file. + +```c# +using System; +using ApiConsumer.GeneratedCode; + +namespace ApiConsumer +{ + class Program + { + static void Main(string[] args) + { + + using (HttpClient httpClient = new HttpClient()) + { + OpenApiClient openApiClient = new OpenApiClient(httpClient); + + // IntelliSense is now available on `openApiClient`! + } + } + } +} +``` diff --git a/docs/usage/toc.md b/docs/usage/toc.md index 76ea777aac..d005d703a5 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -21,7 +21,10 @@ # [Errors](errors.md) # [Metadata](meta.md) # [Caching](caching.md) -# [OpenAPI](openapi.md) + +# OpenAPI +## [Specification Generator](openapi/openapi.md)) +## [Generated Client](openapi/openapiclient.md)) # Extensibility ## [Layer Overview](extensibility/layer-overview.md) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs index ff707e1614..199c33d2ff 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs @@ -57,21 +57,6 @@ public DataContract GetDataContractForType(Type type) return dataContract; } - private static bool IsIdentifiableBaseType(Type type) - { - if (type.IsGenericType) - { - return type.GetGenericTypeDefinition() == typeof(Identifiable<>); - } - - return type == typeof(Identifiable); - } - - private static bool IsIdentity(DataProperty property) - { - return property.MemberInfo.Name == nameof(Identifiable.Id); - } - private static DataContract ReplacePropertiesInDataContract(DataContract dataContract, IEnumerable dataProperties) { return DataContract.ForObject(dataContract.UnderlyingType, dataProperties, dataContract.ObjectExtensionDataType, diff --git a/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj b/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj index 615f85a16e..8d1b0f939a 100644 --- a/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj +++ b/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj @@ -7,7 +7,7 @@ jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net;openapi;swagger;client;nswag - TODO + Contains utility methods to enrich the usability of an OpenAPI gnerated client. json-api-dotnet https://www.jsonapi.net/ MIT diff --git a/test/OpenApiTests/Airplane.cs b/test/OpenApiTests/Airplane.cs index a4c55b30ac..fd75138189 100644 --- a/test/OpenApiTests/Airplane.cs +++ b/test/OpenApiTests/Airplane.cs @@ -10,7 +10,6 @@ namespace OpenApiTests [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Airplane : Identifiable { - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] [Required] [MaxLength(255)] @@ -42,7 +41,3 @@ public sealed class Airplane : Identifiable public ISet Flights { get; set; } } } - - - - diff --git a/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs index 3f4463b60a..c5b7c06cb8 100644 --- a/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs @@ -29,7 +29,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r { Id = airplaneId, Type = AirplanesResourceType.Airplanes, - Attributes = new AirplaneAttributesInPatchRequest() + Attributes = new AirplaneAttributesInPatchRequest { ManufacturedAt = manufacturedAt } diff --git a/test/OpenApiTests/ClientLibrary/RequestTests.cs b/test/OpenApiTests/ClientLibrary/RequestTests.cs index 4dd60e400e..4119b514dd 100644 --- a/test/OpenApiTests/ClientLibrary/RequestTests.cs +++ b/test/OpenApiTests/ClientLibrary/RequestTests.cs @@ -181,7 +181,8 @@ public async Task Partial_patching_resource_produces_expected_request() }; using (apiClient.RegisterAttributesForRequestDocument(requestDocument, - airplane => airplane.ManufacturedAt, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) + airplane => airplane.ManufacturedAt, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, + airplane => airplane.AirtimeInHours)) { // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); @@ -293,9 +294,9 @@ public async Task Patching_ToOne_relationship_produces_expected_request() using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); - var requestDocument = new ToOneAirplaneRequestData() + var requestDocument = new ToOneAirplaneRequestData { - Data = new AirplaneIdentifier() + Data = new AirplaneIdentifier { Id = "bBJHu", Type = AirplanesResourceType.Airplanes @@ -348,7 +349,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); - var requestDocument = new ToManyFlightAttendantRequestData() + var requestDocument = new ToManyFlightAttendantRequestData { Data = new List { @@ -398,7 +399,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); - var requestDocument = new ToManyFlightAttendantRequestData() + var requestDocument = new ToManyFlightAttendantRequestData { Data = new List { @@ -448,7 +449,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); - var requestDocument = new ToManyFlightAttendantRequestData() + var requestDocument = new ToManyFlightAttendantRequestData { Data = new List { diff --git a/test/OpenApiTests/ClientLibrary/ResponseTests.cs b/test/OpenApiTests/ClientLibrary/ResponseTests.cs index bc602a0a02..a6d8e81b8a 100644 --- a/test/OpenApiTests/ClientLibrary/ResponseTests.cs +++ b/test/OpenApiTests/ClientLibrary/ResponseTests.cs @@ -283,7 +283,7 @@ public async Task Posting_resource_translates_response() Type = FlightsResourceType.Flights, Relationships = new FlightRelationshipsInPostRequest { - OperatingAirplane = new ToOneAirplaneRequestData() + OperatingAirplane = new ToOneAirplaneRequestData { Data = new AirplaneIdentifier { @@ -466,9 +466,9 @@ public async Task Patching_ToOne_relationship_translates_response() IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - await apiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData() + await apiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData { - Data = new AirplaneIdentifier() + Data = new AirplaneIdentifier { Id = "Adk2a", Type = AirplanesResourceType.Airplanes @@ -504,7 +504,8 @@ public async Task Getting_ToMany_relationship_translates_response() IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantIdentifierCollectionResponseDocument document = await apiClient.GetFlightFlightAttendantsRelationshipAsync(Convert.ToInt32(flightId)); + FlightAttendantIdentifierCollectionResponseDocument + document = await apiClient.GetFlightFlightAttendantsRelationshipAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().HaveCount(2); @@ -522,7 +523,7 @@ public async Task Posting_ToMany_relationship_produces_empty_response() IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.PostFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData() + Func action = async () => await apiClient.PostFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { @@ -551,7 +552,7 @@ public async Task Patching_ToMany_relationship_produces_empty_response() IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.PatchFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData() + Func action = async () => await apiClient.PatchFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { @@ -580,7 +581,7 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.DeleteFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData() + Func action = async () => await apiClient.DeleteFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { diff --git a/test/OpenApiTests/Flight.cs b/test/OpenApiTests/Flight.cs index 1f185c8c1f..90c61aba92 100644 --- a/test/OpenApiTests/Flight.cs +++ b/test/OpenApiTests/Flight.cs @@ -28,7 +28,6 @@ // [MaxLength(2000)] // public string ExternalId { get; set; } - namespace OpenApiTests { [UsedImplicitly(ImplicitUseTargetFlags.Members)] @@ -45,7 +44,7 @@ public sealed class Flight : Identifiable [Attr] public DateTime? ArrivesAt { get; set; } - [Attr(PublicName = "operated-by", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + [Attr(PublicName = "operated-by", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] public Airline Airline { get; set; } [Attr] diff --git a/test/OpenApiTests/FlightAttendant.cs b/test/OpenApiTests/FlightAttendant.cs index 625a26516b..8fc801b97d 100644 --- a/test/OpenApiTests/FlightAttendant.cs +++ b/test/OpenApiTests/FlightAttendant.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using JetBrains.Annotations; From c6d691dfb435e9ddf807b5538745ad90cdef8c65 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 10 Sep 2021 11:44:38 +0200 Subject: [PATCH 06/35] Adjusted docs and minor refactors --- docs/usage/openapi/client-generator.md | 137 ++++++++++++++++++ .../{openapi.md => openapi-generator.md} | 2 +- docs/usage/openapi/openapiclient.md | 79 ---------- docs/usage/toc.md | 4 +- ...onApiActionDescriptorCollectionProvider.cs | 2 +- .../JsonApiEndpointMetadataProvider.cs | 4 +- .../JsonApiOperationIdSelector.cs | 2 +- .../JsonApiSchemaIdSelector.cs | 2 +- .../JsonApiSchemaGenerator.cs | 15 +- .../ResourceObjectSchemaGenerator.cs | 2 +- .../JsonApiDotNetCore.OpenApiClient.csproj | 2 +- .../GeneratedCode/IOpenApiClient.cs | 1 + .../GeneratedCode/OpenApiClient.cs | 1 + .../ClientLibrary/ResponseTests.cs | 2 - test/OpenApiTests/swagger.json | 13 +- 15 files changed, 154 insertions(+), 114 deletions(-) create mode 100644 docs/usage/openapi/client-generator.md rename docs/usage/openapi/{openapi.md => openapi-generator.md} (98%) delete mode 100644 docs/usage/openapi/openapiclient.md diff --git a/docs/usage/openapi/client-generator.md b/docs/usage/openapi/client-generator.md new file mode 100644 index 0000000000..fa0d3a5054 --- /dev/null +++ b/docs/usage/openapi/client-generator.md @@ -0,0 +1,137 @@ +# Client Generator + +You can you can generate a client library from an OpenAPI specification that describes a JsonApiDotNetCore application. For clients genearted with using [NSwag](http://stevetalkscode.co.uk/openapireference-commands) we provide an additional package that enables partial write requests. + +## Installation + +You are required to install the following NuGet packages: + +- `JsonApiDotNetCore.OpenApiClient` +- `NSwag.ApiDescription.Client` +- `Microsoft.Extensions.ApiDescription.Cient` +- `NSwag.ApiDescription.Client` + +The following examples demonstrate how to install the `JsonApiDotNetCore.OpenApiClient` package. + +### CLI + +``` +dotnet add package JsonApiDotNetCore.OpenApiClient +``` + +### Visual Studio + +```powershell +Install-Package JsonApiDotNetCore.OpenApiClient +``` + +### *.csproj + +```xml + + + + +``` + + +## Adding an OpenApiReference + +Add a reference to your OpenAPI specification in your project file as demonstrated below. + +```xml + + + ApiConsumer.GeneratedCode + OpenApiClient + NSwagCSharp + /UseBaseUrl:false /GenerateClientInterfaces:true + + +``` + + +## Usage + +The NSwag tooling generates the OpenAPI client during a prebuild step. Once your application is built, +you can instantiate it using the class name as indicated in the project file. + +```c# +namespace ApiConsumer +{ + class Program + { + static void Main(string[] args) + { + using (HttpClient httpClient = new HttpClient()) + { + OpenApiClient openApiClient = new OpenApiClient(httpClient); + + // IntelliSense is now available on `openApiClient`! + } + } + } +} +``` + +Support for partial write requests can be enabled by leveraging the extensibility points of the generated client. + +```c# +// Note that this class should be namespace in which NSwag generates the client. +namespace ApiConsumer.GeneratedCode +{ + public partial class OpenApiClient : JsonApiClient + { + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + } + } +} +``` + +You can now perform a write request by calling the `RegisterAttributesForRequest` method. 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 the `alwaysIncludedAttributeSelectors` parameter. + +```c# +// Program.cs +static void Main(string[] args) +{ + using (HttpClient httpClient = new HttpClient()) + { + OpenApiClient openApiClient = new OpenApiClient(httpClient); + + var requestDocument = new ApiResourcePatchRequestDocument + { + Data = new ApiResourceDataInPatchRequest + { + Id = 543, + Type = ApiResourceResourceType.Airplanes, + Attributes = new ApiResourceAttributesInPatchRequest + { + someNullableAttribute = "Value" + } + } + }; + + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, apiResource => apiResource.AnotherNullableAttribute) + { + await apiClient.PatchApiResourceAsync(543, requestDocument)); + + // The request will look like this: + // + // { + // "data": { + // "type": "apiResource", + // "id": "543", + // "attributes": { + // "someNullableAttribute": "Value", + // "anotherNullableAttribute": null, + // } + // } + // } + } + + } +} +``` + diff --git a/docs/usage/openapi/openapi.md b/docs/usage/openapi/openapi-generator.md similarity index 98% rename from docs/usage/openapi/openapi.md rename to docs/usage/openapi/openapi-generator.md index 865f94d1bb..634e4abcd9 100644 --- a/docs/usage/openapi/openapi.md +++ b/docs/usage/openapi/openapi-generator.md @@ -1,4 +1,4 @@ -# OpenAPI +# OpenAPI generator You can describe your API with an OpenAPI specification using the [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) integration for JsonApiDotNetCore. diff --git a/docs/usage/openapi/openapiclient.md b/docs/usage/openapi/openapiclient.md deleted file mode 100644 index b61644f919..0000000000 --- a/docs/usage/openapi/openapiclient.md +++ /dev/null @@ -1,79 +0,0 @@ -# OpenAPI Generated Client - -Given an OpenAPI specification, you can generate a client library using [NSwag](http://stevetalkscode.co.uk/openapireference-commands). We provide additional methods to enrich the features of this client. - -## Installation - -You need to install the following NuGet packages: - -- `JsonApiDotNetCore.OpenApiClient` -- `NSwag.ApiDescription.Client` -- `Microsoft.Extensions.ApiDescription.Cient` -- `NSwag.ApiDescription.Client` - -The following examples demonstrate how to install the `JsonApiDotNetCore.OpenApiClient` package. - -### CLI - -``` -dotnet add package JsonApiDotNetCore.OpenApiClient -``` - -### Visual Studio - -```powershell -Install-Package JsonApiDotNetCore.OpenApiClient -``` - -### *.csproj - -```xml - - - - -``` - - -## OpenApiReference - -Add a reference to your OpenAPI specification in your project file as demonstrated below. - -```xml - - - YourApplication.GeneratedCode - OpenApiClient - NSwagCSharp - /UseBaseUrl:false /GenerateClientInterfaces:true - - -``` - - -## Usage - -The NSwag tooling generates the OpenAPI client during a prebuild step. Once your application is built, -you can instantiate it using the class name as indicated in the project file. - -```c# -using System; -using ApiConsumer.GeneratedCode; - -namespace ApiConsumer -{ - class Program - { - static void Main(string[] args) - { - - using (HttpClient httpClient = new HttpClient()) - { - OpenApiClient openApiClient = new OpenApiClient(httpClient); - - // IntelliSense is now available on `openApiClient`! - } - } - } -} -``` diff --git a/docs/usage/toc.md b/docs/usage/toc.md index d005d703a5..d39acabf43 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -23,8 +23,8 @@ # [Caching](caching.md) # OpenAPI -## [Specification Generator](openapi/openapi.md)) -## [Generated Client](openapi/openapiclient.md)) +## [Describing Your API](openapi/openapi-generator.md)) +## [Generating A Client](openapi/client-generator.md)) # Extensibility ## [Layer Overview](extensibility/layer-overview.md) diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs index ec90f37d6e..3821ad5264 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.OpenApi.JsonApiMetadata; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.OpenApi.JsonApiMetadata; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Controllers; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs index 768098ebc1..6a2769d829 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; using JsonApiDotNetCore.Resources.Annotations; namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs index a880d6ce5c..bb71589260 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.Linq; using Humanizer; +using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; -using JsonApiDotNetCore.Middleware; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Controllers; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs index 9ded9e05b4..1907573190 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Linq; using Humanizer; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; -using JsonApiDotNetCore.Configuration; namespace JsonApiDotNetCore.OpenApi { diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index a27f7915c9..3b506a86b9 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -1,10 +1,10 @@ using System; using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.OpenApi.JsonApiObjects; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; -using JsonApiDotNetCore.Configuration; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -66,16 +66,9 @@ public OpenApiSchema GenerateSchema(Type type, SchemaRepository schemaRepository return jsonApiDocumentSchema; } - OpenApiSchema schema; - - if (IsJsonApiResourceDocument(type)) - { - schema = GenerateResourceJsonApiDocumentSchema(type); - } - else - { - schema = _defaultSchemaGenerator.GenerateSchema(type, schemaRepository, memberInfo, parameterInfo); - } + OpenApiSchema schema = IsJsonApiResourceDocument(type) + ? GenerateResourceJsonApiDocumentSchema(type) + : _defaultSchemaGenerator.GenerateSchema(type, schemaRepository, memberInfo, parameterInfo); if (IsSingleNonPrimaryDataDocument(type)) { diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index f0f65fd21f..8deaab7d6e 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using Microsoft.OpenApi.Models; using Newtonsoft.Json.Serialization; using Swashbuckle.AspNetCore.SwaggerGen; diff --git a/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj b/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj index 8d1b0f939a..5ac19d01d4 100644 --- a/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj +++ b/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj @@ -7,7 +7,7 @@ jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net;openapi;swagger;client;nswag - Contains utility methods to enrich the usability of an OpenAPI gnerated client. + Contains utility methods to enrich the usability of an OpenAPI generated client. json-api-dotnet https://www.jsonapi.net/ MIT diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs b/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs index 394714bc32..3ff3230060 100644 --- a/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs +++ b/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs @@ -2,6 +2,7 @@ namespace OpenApiTests.ClientLibrary.GeneratedCode { + // ReSharper disable once MemberCanBeInternal partial interface IOpenApiClient : IJsonApiClient { } diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs b/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs index bf94af876c..411250efc6 100644 --- a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs +++ b/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs @@ -3,6 +3,7 @@ namespace OpenApiTests.ClientLibrary.GeneratedCode { + // ReSharper disable once MemberCanBeInternal public partial class OpenApiClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) diff --git a/test/OpenApiTests/ClientLibrary/ResponseTests.cs b/test/OpenApiTests/ClientLibrary/ResponseTests.cs index a6d8e81b8a..949566ab1a 100644 --- a/test/OpenApiTests/ClientLibrary/ResponseTests.cs +++ b/test/OpenApiTests/ClientLibrary/ResponseTests.cs @@ -121,7 +121,6 @@ public async Task Getting_resource_collection_translates_response() flight.Meta["docs"].Should().Be(flightMetaValue); flight.Attributes.Destination.Should().Be(flightDestination); - flight.Attributes.FlightNumber.Should().Be(null); flight.Attributes.ServicesOnBoard.Should().HaveCount(3); flight.Attributes.ServicesOnBoard.ElementAt(0).Should().Be(fightServiceOnBoard); flight.Attributes.ServicesOnBoard.ElementAt(1).Should().Be(string.Empty); @@ -187,7 +186,6 @@ 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.FlightNumber.Should().BeNull(); document.Data.Attributes.ServicesOnBoard.Should().BeNull(); document.Data.Attributes.Destination.Should().BeNull(); document.Data.Attributes.OperatedBy.Should().Be(default(Airline)); diff --git a/test/OpenApiTests/swagger.json b/test/OpenApiTests/swagger.json index c1306eebbd..6995c9d1c7 100644 --- a/test/OpenApiTests/swagger.json +++ b/test/OpenApiTests/swagger.json @@ -1,4 +1,3 @@ - { "openapi": "3.0.1", "info": { @@ -2446,11 +2445,6 @@ "maxLength": 40, "type": "string" }, - "flight-number": { - "maxLength": 8, - "type": "string", - "nullable": true - }, "operated-by": { "$ref": "#/components/schemas/airline" } @@ -2464,11 +2458,6 @@ "maxLength": 40, "type": "string" }, - "flight-number": { - "maxLength": 8, - "type": "string", - "nullable": true - }, "departs-at": { "type": "string", "format": "date-time", @@ -3016,4 +3005,4 @@ } } } -} \ No newline at end of file +} From ef2753ee5218224ddbf726c23f8fe0ca18b86e64 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 10 Sep 2021 17:25:07 +0200 Subject: [PATCH 07/35] Replace resource name formatter with internal one --- .../JsonApiOperationIdSelector.cs | 5 +- .../JsonApiSchemaIdSelector.cs | 4 +- .../ResourceNameFormatterProxy.cs | 50 ------------------- .../ServiceCollectionExtensions.cs | 2 +- .../ResourceObjectSchemaGenerator.cs | 4 +- 5 files changed, 8 insertions(+), 57 deletions(-) delete mode 100644 src/JsonApiDotNetCore.OpenApi/ResourceNameFormatterProxy.cs diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs index bb71589260..36d6dc6d06 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Humanizer; +using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Middleware; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; @@ -35,7 +36,7 @@ internal sealed class JsonApiOperationIdSelector private readonly IControllerResourceMapping _controllerResourceMapping; private readonly NamingStrategy _namingStrategy; - private readonly ResourceNameFormatterProxy _formatter; + private readonly ResourceNameFormatter _formatter; public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, NamingStrategy namingStrategy) { @@ -44,7 +45,7 @@ public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceM _controllerResourceMapping = controllerResourceMapping; _namingStrategy = namingStrategy; - _formatter = new ResourceNameFormatterProxy(namingStrategy); + _formatter = new ResourceNameFormatter(namingStrategy); } public string GetOperationId(ApiDescription endpoint) diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs index 1907573190..a9dac443b0 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -30,10 +30,10 @@ internal sealed class JsonApiSchemaIdSelector [typeof(ResourceIdentifierObject<>)] = "###-identifier" }; - private readonly ResourceNameFormatterProxy _formatter; + private readonly ResourceNameFormatter _formatter; private readonly IResourceContextProvider _resourceContextProvider; - public JsonApiSchemaIdSelector(ResourceNameFormatterProxy formatter, IResourceContextProvider resourceContextProvider) + public JsonApiSchemaIdSelector(ResourceNameFormatter formatter, IResourceContextProvider resourceContextProvider) { ArgumentGuard.NotNull(formatter, nameof(formatter)); ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider)); diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceNameFormatterProxy.cs b/src/JsonApiDotNetCore.OpenApi/ResourceNameFormatterProxy.cs deleted file mode 100644 index c291ba36f6..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/ResourceNameFormatterProxy.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Reflection; -using JsonApiDotNetCore.Resources; -using Newtonsoft.Json.Serialization; - -namespace JsonApiDotNetCore.OpenApi -{ - internal sealed class ResourceNameFormatterProxy - { - private const string ResourceNameFormatterTypeName = "JsonApiDotNetCore.Configuration.ResourceNameFormatter"; - private const string FormatResourceNameMethodName = "FormatResourceName"; - - private readonly NamingStrategy _namingStrategy; - private readonly Type _resourceNameFormatterType; - private readonly MethodInfo _formatResourceNameMethod; - - public ResourceNameFormatterProxy(NamingStrategy namingStrategy) - { - ArgumentGuard.NotNull(namingStrategy, nameof(namingStrategy)); - - _namingStrategy = namingStrategy; - - _resourceNameFormatterType = typeof(IIdentifiable).Assembly.GetType(ResourceNameFormatterTypeName); - - if (_resourceNameFormatterType == null) - { - throw new InvalidOperationException($"Failed to locate '{ResourceNameFormatterTypeName}'."); - } - - _formatResourceNameMethod = _resourceNameFormatterType.GetMethod(FormatResourceNameMethodName); - - if (_formatResourceNameMethod == null) - { - throw new InvalidOperationException($"Failed to locate '{ResourceNameFormatterTypeName}.{FormatResourceNameMethodName}'."); - } - } - - public string FormatResourceName(Type type) - { - ArgumentGuard.NotNull(type, nameof(type)); - - object resourceNameFormatter = Activator.CreateInstance(_resourceNameFormatterType, _namingStrategy); - - return (string)_formatResourceNameMethod.Invoke(resourceNameFormatter, new object[] - { - type - }); - } - } -} diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index 8753d3747d..3a14f4de9a 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -104,7 +104,7 @@ private static IList GetOperationTags(ApiDescription description, IContr private static void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions, IResourceContextProvider resourceContextProvider, NamingStrategy namingStrategy) { - ResourceNameFormatterProxy resourceNameFormatter = new(namingStrategy); + ResourceNameFormatter resourceNameFormatter = new(namingStrategy); JsonApiSchemaIdSelector jsonApiObjectSchemaSelector = new(resourceNameFormatter, resourceContextProvider); swaggerGenOptions.CustomSchemaIds(type => jsonApiObjectSchemaSelector.GetSchemaId(type)); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs index 8deaab7d6e..e4c3791e20 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceObjectSchemaGenerator.cs @@ -41,8 +41,8 @@ private static Func CreateFi ResourceTypeSchemaGenerator resourceTypeSchemaGenerator) { NamingStrategy namingStrategy = ((DefaultContractResolver)jsonApiOptions.SerializerSettings.ContractResolver)!.NamingStrategy; - ResourceNameFormatterProxy resourceNameFormatterProxy = new(namingStrategy); - var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatterProxy, resourceContextProvider); + ResourceNameFormatter resourceNameFormatter = new(namingStrategy); + var jsonApiSchemaIdSelector = new JsonApiSchemaIdSelector(resourceNameFormatter, resourceContextProvider); return resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor, defaultSchemaGenerator, jsonApiSchemaIdSelector, resourceTypeSchemaGenerator); From b3cdc7ab6cff1432e11888d56991e423959e3d14 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 10 Sep 2021 19:55:24 +0200 Subject: [PATCH 08/35] Removed JsonApiInputFormatter workaround --- .../JsonApiInputFormatterWithMetadata.cs | 41 -------------- .../JsonApiRequestFormatMetadataProvider.cs | 54 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 10 +--- 3 files changed, 56 insertions(+), 49 deletions(-) delete mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiInputFormatterWithMetadata.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiInputFormatterWithMetadata.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiInputFormatterWithMetadata.cs deleted file mode 100644 index b57ee6decd..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiInputFormatterWithMetadata.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Serialization; -using Microsoft.AspNetCore.Mvc.ApiExplorer; -using Microsoft.AspNetCore.Mvc.Formatters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Net.Http.Headers; - -namespace JsonApiDotNetCore.OpenApi -{ - internal sealed class JsonApiInputFormatterWithMetadata : IJsonApiInputFormatter, IApiRequestFormatMetadataProvider - { - public bool CanRead(InputFormatterContext context) - { - ArgumentGuard.NotNull(context, nameof(context)); - - return context.HttpContext.IsJsonApiRequest(); - } - - public async Task ReadAsync(InputFormatterContext context) - { - ArgumentGuard.NotNull(context, nameof(context)); - - var reader = context.HttpContext.RequestServices.GetRequiredService(); - return await reader.ReadAsync(context); - } - - public IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) - { - ArgumentGuard.NotNullNorEmpty(contentType, nameof(contentType)); - ArgumentGuard.NotNull(objectType, nameof(objectType)); - - return new MediaTypeCollection - { - new MediaTypeHeaderValue(HeaderConstants.MediaType) - }; - } - } -} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs new file mode 100644 index 0000000000..1fef9854f0 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using JsonApiDotNetCore.Middleware; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; + +namespace JsonApiDotNetCore.OpenApi +{ + internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IApiRequestFormatMetadataProvider + { + private static readonly Type[] JsonApiRequestObjectOpenType = + { + typeof(ToManyRelationshipRequestData<>), + typeof(ToOneRelationshipRequestData<>), + typeof(ResourcePostRequestDocument<>), + typeof(ResourcePatchRequestDocument<>) + }; + + /// + public bool CanRead(InputFormatterContext context) + { + return false; + } + + /// + public Task ReadAsync(InputFormatterContext context) + { + throw new UnreachableCodeException(); + } + + /// + public IReadOnlyList GetSupportedContentTypes(string contentType, Type objectType) + { + ArgumentGuard.NotNullNorEmpty(contentType, nameof(contentType)); + ArgumentGuard.NotNull(objectType, nameof(objectType)); + + if (contentType == HeaderConstants.MediaType && objectType.IsGenericType && + JsonApiRequestObjectOpenType.Contains(objectType.GetGenericTypeDefinition())) + { + return new MediaTypeCollection + { + new MediaTypeHeaderValue(HeaderConstants.MediaType) + }; + } + + return new MediaTypeCollection(); + } + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs index 3a14f4de9a..f86288113a 100644 --- a/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs @@ -32,8 +32,6 @@ public static void AddOpenApi(this IServiceCollection services, IMvcCoreBuilder AddSwaggerGenerator(scope, services, setupSwaggerGenAction); AddSwashbuckleCliCompatibility(scope, mvcBuilder); AddOpenApiEndpointConvention(scope, mvcBuilder); - - AddJsonApiInputFormatterWorkaround(mvcBuilder); } private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBuilder mvcBuilder) @@ -52,6 +50,8 @@ private static void AddCustomApiExplorer(IServiceCollection services, IMvcCoreBu }); mvcBuilder.AddApiExplorer(); + + mvcBuilder.AddMvcOptions(options => options.InputFormatters.Add(new JsonApiRequestFormatMetadataProvider())); } private static void AddSwaggerGenerator(IServiceScope scope, IServiceCollection services, Action setupSwaggerGenAction) @@ -132,11 +132,5 @@ private static void AddOpenApiEndpointConvention(IServiceScope scope, IMvcCoreBu mvcBuilder.AddMvcOptions(options => options.Conventions.Add(new OpenApiEndpointConvention(resourceContextProvider, controllerResourceMapping))); } - - private static void AddJsonApiInputFormatterWorkaround(IMvcCoreBuilder mvcBuilder) - { - // See https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/972 for why this is needed. - mvcBuilder.AddMvcOptions(options => options.InputFormatters.Add(new JsonApiInputFormatterWithMetadata())); - } } } From bad9aad2e0813e13582a4f8e42378e778d9d1392 Mon Sep 17 00:00:00 2001 From: maurei Date: Tue, 14 Sep 2021 15:40:55 +0200 Subject: [PATCH 09/35] Moved full document test to ExistingOpenApiIntegration test folder, which is to be removed at the very end once have dedicated test suites for all components --- .../AircraftType.cs | 2 +- .../Airline.cs | 2 +- .../Airplane.cs | 2 +- .../AirplanesController.cs | 2 +- .../ExistingIntegrationDbContext.cs} | 6 ++-- .../ExistingOpenApiIntegrationStartup.cs | 30 +++++++++++++++++++ .../ExistingOpenApiIntegrationTests.cs} | 9 +++--- .../Flight.cs | 25 +--------------- .../FlightAttendant.cs | 4 +-- .../FlightAttendantExpertiseLevel.cs} | 4 +-- .../FlightAttendantsController.cs | 2 +- .../FlightsController.cs | 2 +- .../swagger.json | 0 test/OpenApiTests/OpenApiTests.csproj | 6 ++-- .../{ => Startups}/OpenApiStartup.cs | 24 ++------------- 15 files changed, 54 insertions(+), 66 deletions(-) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/AircraftType.cs (80%) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/Airline.cs (79%) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/Airplane.cs (97%) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/AirplanesController.cs (90%) rename test/OpenApiTests/{OpenApiDbContext.cs => ExistingOpenApiIntegration/ExistingIntegrationDbContext.cs} (82%) create mode 100644 test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs rename test/OpenApiTests/{OpenApiDocumentTests.cs => ExistingOpenApiIntegration/ExistingOpenApiIntegrationTests.cs} (84%) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/Flight.cs (54%) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/FlightAttendant.cs (91%) rename test/OpenApiTests/{FlightAttendantExpertise.cs => ExistingOpenApiIntegration/FlightAttendantExpertiseLevel.cs} (63%) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/FlightAttendantsController.cs (90%) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/FlightsController.cs (89%) rename test/OpenApiTests/{ => ExistingOpenApiIntegration}/swagger.json (100%) rename test/OpenApiTests/{ => Startups}/OpenApiStartup.cs (51%) diff --git a/test/OpenApiTests/AircraftType.cs b/test/OpenApiTests/ExistingOpenApiIntegration/AircraftType.cs similarity index 80% rename from test/OpenApiTests/AircraftType.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/AircraftType.cs index 7cf37c9b25..56da2f05b3 100644 --- a/test/OpenApiTests/AircraftType.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/AircraftType.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum AircraftType diff --git a/test/OpenApiTests/Airline.cs b/test/OpenApiTests/ExistingOpenApiIntegration/Airline.cs similarity index 79% rename from test/OpenApiTests/Airline.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/Airline.cs index 86e1b94ad0..5eb49adec4 100644 --- a/test/OpenApiTests/Airline.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/Airline.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum Airline diff --git a/test/OpenApiTests/Airplane.cs b/test/OpenApiTests/ExistingOpenApiIntegration/Airplane.cs similarity index 97% rename from test/OpenApiTests/Airplane.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/Airplane.cs index fd75138189..98a21686b5 100644 --- a/test/OpenApiTests/Airplane.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/Airplane.cs @@ -5,7 +5,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Airplane : Identifiable diff --git a/test/OpenApiTests/AirplanesController.cs b/test/OpenApiTests/ExistingOpenApiIntegration/AirplanesController.cs similarity index 90% rename from test/OpenApiTests/AirplanesController.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/AirplanesController.cs index 104f39a1eb..9666bba7db 100644 --- a/test/OpenApiTests/AirplanesController.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/AirplanesController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { public sealed class AirplanesController : JsonApiController { diff --git a/test/OpenApiTests/OpenApiDbContext.cs b/test/OpenApiTests/ExistingOpenApiIntegration/ExistingIntegrationDbContext.cs similarity index 82% rename from test/OpenApiTests/OpenApiDbContext.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/ExistingIntegrationDbContext.cs index 175450d23c..ef534022ab 100644 --- a/test/OpenApiTests/OpenApiDbContext.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/ExistingIntegrationDbContext.cs @@ -4,16 +4,16 @@ // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class OpenApiDbContext : DbContext + public sealed class ExistingIntegrationDbContext : DbContext { public DbSet Airplanes { get; set; } public DbSet Flights { get; set; } public DbSet FlightAttendants { get; set; } - public OpenApiDbContext(DbContextOptions options) + public ExistingIntegrationDbContext(DbContextOptions options) : base(options) { } diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs b/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs new file mode 100644 index 0000000000..c91883c05c --- /dev/null +++ b/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs @@ -0,0 +1,30 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Resources.Annotations; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json.Serialization; +using OpenApiTests.Startups; + +namespace OpenApiTests.ExistingOpenApiIntegration +{ + public sealed class ExistingOpenApiIntegrationStartup : OpenApiStartup + where TDbContext : DbContext + { + protected override void SetJsonApiOptions(JsonApiOptions options) + { + base.SetJsonApiOptions(options); + + options.Namespace = "api/v1"; + options.DefaultPageSize = new PageSize(10); + options.MaximumPageSize = new PageSize(100); + options.MaximumPageNumber = new PageNumber(50); + options.IncludeTotalResourceCount = true; + options.ValidateModelState = true; + options.DefaultAttrCapabilities = AttrCapabilities.AllowView; + + options.SerializerSettings.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new KebabCaseNamingStrategy() + }; + } + } +} diff --git a/test/OpenApiTests/OpenApiDocumentTests.cs b/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationTests.cs similarity index 84% rename from test/OpenApiTests/OpenApiDocumentTests.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationTests.cs index 0711bb2f04..b23398e901 100644 --- a/test/OpenApiTests/OpenApiDocumentTests.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationTests.cs @@ -7,11 +7,12 @@ using TestBuildingBlocks; using Xunit; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { - public sealed class OpenApiDocumentTests : IntegrationTestContext, OpenApiDbContext> + public sealed class ExistingOpenApiIntegrationTests + : IntegrationTestContext, ExistingIntegrationDbContext> { - public OpenApiDocumentTests() + public ExistingOpenApiIntegrationTests() { UseController(); UseController(); @@ -22,7 +23,7 @@ public OpenApiDocumentTests() public async Task Retrieved_document_matches_expected_document() { // Arrange - string embeddedResourceName = $"{nameof(OpenApiTests)}.swagger.json"; + string embeddedResourceName = $"{nameof(OpenApiTests)}.{nameof(ExistingOpenApiIntegration)}.swagger.json"; string expectedDocument = await LoadEmbeddedResourceAsync(embeddedResourceName); const string requestUrl = "swagger/v1/swagger.json"; diff --git a/test/OpenApiTests/Flight.cs b/test/OpenApiTests/ExistingOpenApiIntegration/Flight.cs similarity index 54% rename from test/OpenApiTests/Flight.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/Flight.cs index 90c61aba92..84b8bc2908 100644 --- a/test/OpenApiTests/Flight.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/Flight.cs @@ -5,30 +5,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -// -// -// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] -// [Required, MaxLength(255)] -// public string Title { get; set; } -// -// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] -// [MaxLength(2000)] -// public string Summary { get; set; } -// -// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] -// public int? DurationInSeconds { get; set; } -// -// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] -// public DateTime? PublishedAt { get; set; } -// -// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] -// public bool IsDeleted { get; set; } -// -// [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] -// [MaxLength(2000)] -// public string ExternalId { get; set; } - -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Flight : Identifiable diff --git a/test/OpenApiTests/FlightAttendant.cs b/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendant.cs similarity index 91% rename from test/OpenApiTests/FlightAttendant.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendant.cs index 8fc801b97d..7698c62912 100644 --- a/test/OpenApiTests/FlightAttendant.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendant.cs @@ -4,7 +4,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class FlightAttendant : Identifiable @@ -13,7 +13,7 @@ public sealed class FlightAttendant : Identifiable public override string Id { get; set; } [Attr(Capabilities = AttrCapabilities.None)] - public FlightAttendantExpertise ExpertiseLevel { get; set; } + public FlightAttendantExpertiseLevel ExpertiseLevel { get; set; } [Attr(PublicName = "document-number", Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] [Required] diff --git a/test/OpenApiTests/FlightAttendantExpertise.cs b/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantExpertiseLevel.cs similarity index 63% rename from test/OpenApiTests/FlightAttendantExpertise.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantExpertiseLevel.cs index d5f647d3b8..fe23c1e4b5 100644 --- a/test/OpenApiTests/FlightAttendantExpertise.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantExpertiseLevel.cs @@ -1,9 +1,9 @@ using JetBrains.Annotations; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public enum FlightAttendantExpertise + public enum FlightAttendantExpertiseLevel { Junior, Intermediate, diff --git a/test/OpenApiTests/FlightAttendantsController.cs b/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantsController.cs similarity index 90% rename from test/OpenApiTests/FlightAttendantsController.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantsController.cs index aed6adea28..d3c38a1d67 100644 --- a/test/OpenApiTests/FlightAttendantsController.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantsController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { public sealed class FlightAttendantsController : JsonApiController { diff --git a/test/OpenApiTests/FlightsController.cs b/test/OpenApiTests/ExistingOpenApiIntegration/FlightsController.cs similarity index 89% rename from test/OpenApiTests/FlightsController.cs rename to test/OpenApiTests/ExistingOpenApiIntegration/FlightsController.cs index 3b226673a2..74bff6612d 100644 --- a/test/OpenApiTests/FlightsController.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/FlightsController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace OpenApiTests +namespace OpenApiTests.ExistingOpenApiIntegration { public sealed class FlightsController : JsonApiController { diff --git a/test/OpenApiTests/swagger.json b/test/OpenApiTests/ExistingOpenApiIntegration/swagger.json similarity index 100% rename from test/OpenApiTests/swagger.json rename to test/OpenApiTests/ExistingOpenApiIntegration/swagger.json diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index 366fecc5e5..fced745af1 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -29,16 +29,16 @@ - + OpenApiTests.ClientLibrary.GeneratedCode OpenApiClient NSwagCSharp /UseBaseUrl:false /GenerateClientInterfaces:true - + - + diff --git a/test/OpenApiTests/OpenApiStartup.cs b/test/OpenApiTests/Startups/OpenApiStartup.cs similarity index 51% rename from test/OpenApiTests/OpenApiStartup.cs rename to test/OpenApiTests/Startups/OpenApiStartup.cs index b4ce081d3f..df477372cd 100644 --- a/test/OpenApiTests/OpenApiStartup.cs +++ b/test/OpenApiTests/Startups/OpenApiStartup.cs @@ -1,17 +1,15 @@ using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.OpenApi; -using JsonApiDotNetCore.Resources.Annotations; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json.Serialization; using TestBuildingBlocks; -namespace OpenApiTests +namespace OpenApiTests.Startups { - public sealed class OpenApiStartup : TestableStartup + public abstract class OpenApiStartup : TestableStartup where TDbContext : DbContext { public override void ConfigureServices(IServiceCollection services) @@ -23,24 +21,6 @@ public override void ConfigureServices(IServiceCollection services) services.AddOpenApi(mvcBuilder); } - protected override void SetJsonApiOptions(JsonApiOptions options) - { - base.SetJsonApiOptions(options); - - options.Namespace = "api/v1"; - options.DefaultPageSize = new PageSize(10); - options.MaximumPageSize = new PageSize(100); - options.MaximumPageNumber = new PageNumber(50); - options.IncludeTotalResourceCount = true; - options.ValidateModelState = true; - options.DefaultAttrCapabilities = AttrCapabilities.AllowView; - - options.SerializerSettings.ContractResolver = new DefaultContractResolver - { - NamingStrategy = new KebabCaseNamingStrategy() - }; - } - public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory) { app.UseRouting(); From ea46194660d794f78c8320cb3b0b6b78a5c00f66 Mon Sep 17 00:00:00 2001 From: maurei Date: Tue, 14 Sep 2021 21:37:05 +0200 Subject: [PATCH 10/35] Disabled OpenAPI for nonjson and operations controller, as these are not yet supported and will cause the integration to crash --- .../JsonApiMetadata/EndpointResolver.cs | 7 +++++++ .../ExistingOpenApiIntegrationStartup.cs | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs index ee17274a0d..810c96ab69 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Reflection; +using JsonApiDotNetCore.Controllers; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; @@ -11,6 +12,12 @@ internal sealed class EndpointResolver { ArgumentGuard.NotNull(controllerAction, nameof(controllerAction)); + if (!typeof(CoreJsonApiController).IsAssignableFrom(controllerAction.ReflectedType) || + typeof(BaseJsonApiOperationsController).IsAssignableFrom(controllerAction.ReflectedType)) + { + return null; + } + HttpMethodAttribute method = controllerAction.GetCustomAttributes(true).OfType().FirstOrDefault(); return ResolveJsonApiEndpoint(method); diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs b/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs index c91883c05c..ec13d3e12e 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs +++ b/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs @@ -1,3 +1,4 @@ +using JetBrains.Annotations; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; @@ -6,6 +7,7 @@ namespace OpenApiTests.ExistingOpenApiIntegration { + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] public sealed class ExistingOpenApiIntegrationStartup : OpenApiStartup where TDbContext : DbContext { From a0d5bff152992591db9764ba96aedf799664d549 Mon Sep 17 00:00:00 2001 From: maurei Date: Tue, 14 Sep 2021 22:18:40 +0200 Subject: [PATCH 11/35] review feedback on docs --- ...penapi-generator.md => api-description.md} | 2 +- ...{client-generator.md => client-library.md} | 44 +++++++++---------- docs/usage/toc.md | 4 +- 3 files changed, 25 insertions(+), 25 deletions(-) rename docs/usage/openapi/{openapi-generator.md => api-description.md} (98%) rename docs/usage/openapi/{client-generator.md => client-library.md} (61%) diff --git a/docs/usage/openapi/openapi-generator.md b/docs/usage/openapi/api-description.md similarity index 98% rename from docs/usage/openapi/openapi-generator.md rename to docs/usage/openapi/api-description.md index 634e4abcd9..85bc0f62cb 100644 --- a/docs/usage/openapi/openapi-generator.md +++ b/docs/usage/openapi/api-description.md @@ -1,4 +1,4 @@ -# OpenAPI generator +# OpenAPI API Description You can describe your API with an OpenAPI specification using the [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) integration for JsonApiDotNetCore. diff --git a/docs/usage/openapi/client-generator.md b/docs/usage/openapi/client-library.md similarity index 61% rename from docs/usage/openapi/client-generator.md rename to docs/usage/openapi/client-library.md index fa0d3a5054..129f9ac3ca 100644 --- a/docs/usage/openapi/client-generator.md +++ b/docs/usage/openapi/client-library.md @@ -1,6 +1,6 @@ -# Client Generator +# OpenAPI Client Library -You can you can generate a client library from an OpenAPI specification that describes a JsonApiDotNetCore application. For clients genearted with using [NSwag](http://stevetalkscode.co.uk/openapireference-commands) we provide an additional package that enables partial write requests. +You can generate a client library in various programming languages from the OpenAPI specification file that JsonApiDotNetCore APIs provide. For C# .NET clients generated using NSwag, we provide an additional package that introduces support for sending partial write requests. ## Installation @@ -42,8 +42,8 @@ Add a reference to your OpenAPI specification in your project file as demonstrat ```xml - ApiConsumer.GeneratedCode - OpenApiClient + JsonApiDotNetCoreExampleClient.GeneratedCode + ExampleApiClient NSwagCSharp /UseBaseUrl:false /GenerateClientInterfaces:true @@ -57,7 +57,7 @@ The NSwag tooling generates the OpenAPI client during a prebuild step. Once your you can instantiate it using the class name as indicated in the project file. ```c# -namespace ApiConsumer +namespace JsonApiDotNetCoreExampleClient { class Program { @@ -65,9 +65,9 @@ namespace ApiConsumer { using (HttpClient httpClient = new HttpClient()) { - OpenApiClient openApiClient = new OpenApiClient(httpClient); + ExampleApiClient exampleApiClient = new ExampleApiClient(httpClient); - // IntelliSense is now available on `openApiClient`! + // IntelliSense is now available on `exampleApiClient`! } } } @@ -77,10 +77,10 @@ namespace ApiConsumer Support for partial write requests can be enabled by leveraging the extensibility points of the generated client. ```c# -// Note that this class should be namespace in which NSwag generates the client. -namespace ApiConsumer.GeneratedCode +namespace JsonApiDotNetCoreExampleClient.GeneratedCode { - public partial class OpenApiClient : JsonApiClient + // Note that this class should be in the same namespace as the ExampleApiClient generated by NSwag. + public partial class ExampleApiClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) { @@ -98,34 +98,34 @@ static void Main(string[] args) { using (HttpClient httpClient = new HttpClient()) { - OpenApiClient openApiClient = new OpenApiClient(httpClient); + ExampleApiClient exampleApiClient = new ExampleApiClient(httpClient); - var requestDocument = new ApiResourcePatchRequestDocument + var requestDocument = new PersonPatchRequestDocument { - Data = new ApiResourceDataInPatchRequest + Data = new PersonDataInPatchRequest { - Id = 543, - Type = ApiResourceResourceType.Airplanes, - Attributes = new ApiResourceAttributesInPatchRequest + Id = "546", + Type = PersonResourceType.People, + Attributes = new PersonAttributesInPatchRequest { - someNullableAttribute = "Value" + FirstName = "Jack" } } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, apiResource => apiResource.AnotherNullableAttribute) + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, person => person.LastName) { - await apiClient.PatchApiResourceAsync(543, requestDocument)); + await exampleApiClient.PatchPersonAsync(543, requestDocument)); // The request will look like this: // // { // "data": { - // "type": "apiResource", + // "type": "people", // "id": "543", // "attributes": { - // "someNullableAttribute": "Value", - // "anotherNullableAttribute": null, + // "firstName": "Jack", + // "lastName": null, // } // } // } diff --git a/docs/usage/toc.md b/docs/usage/toc.md index d39acabf43..fb881c7a58 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -23,8 +23,8 @@ # [Caching](caching.md) # OpenAPI -## [Describing Your API](openapi/openapi-generator.md)) -## [Generating A Client](openapi/client-generator.md)) +## [API Description](openapi/api-description.md)) +## [Client Library](openapi/client-library.md)) # Extensibility ## [Layer Overview](extensibility/layer-overview.md) From 280e449365527ba7e1989767e1d926c039882c91 Mon Sep 17 00:00:00 2001 From: maurei Date: Tue, 14 Sep 2021 22:56:28 +0200 Subject: [PATCH 12/35] Add a very minimal example of a generated OpenAPI client for JsonApiDotNetCoreExample project. The only call that currently doesn't crash is the one associated to deleting a primary resource --- JsonApiDotNetCore.sln | 15 + .../GeneratedCode/ExampleApiClient.cs | 15 + .../GeneratedCode/IExampleApiClient.cs | 8 + .../JsonApiDotNetCoreExampleClient.csproj | 31 + .../JsonApiDotNetCoreExampleClient/Program.cs | 35 + .../swagger.json | 2653 +++++++++++++++++ 6 files changed, 2757 insertions(+) create mode 100644 src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs create mode 100644 src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs create mode 100644 src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj create mode 100644 src/Examples/JsonApiDotNetCoreExampleClient/Program.cs create mode 100644 src/Examples/JsonApiDotNetCoreExampleClient/swagger.json diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln index 1b115959f6..e282f88712 100644 --- a/JsonApiDotNetCore.sln +++ b/JsonApiDotNetCore.sln @@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiTests", "test\OpenAp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApiClient", "src\JsonApiDotNetCore.OpenApiClient\JsonApiDotNetCore.OpenApiClient.csproj", "{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreExampleClient", "src\Examples\JsonApiDotNetCoreExampleClient\JsonApiDotNetCoreExampleClient.csproj", "{7FC5DFA3-6F66-4FD8-820D-81E93856F252}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -252,6 +254,18 @@ Global {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x64.Build.0 = Release|Any CPU {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x86.ActiveCfg = Release|Any CPU {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x86.Build.0 = Release|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|x64.ActiveCfg = Debug|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|x64.Build.0 = Debug|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|x86.ActiveCfg = Debug|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|x86.Build.0 = Debug|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|Any CPU.Build.0 = Release|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x64.ActiveCfg = Release|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x64.Build.0 = Release|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x86.ActiveCfg = Release|Any CPU + {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -273,6 +287,7 @@ Global {71287D6F-6C3B-44B4-9FCA-E78FE3F02289} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} {B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} + {7FC5DFA3-6F66-4FD8-820D-81E93856F252} = {026FBC6C-AF76-4568-9B87-EC73457899FD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs new file mode 100644 index 0000000000..c9dd703249 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.OpenApiClient; +using Newtonsoft.Json; + +namespace JsonApiDotNetCoreExampleClient.GeneratedCode +{ + public partial class ExampleApiClient : JsonApiClient + { + partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) + { + SetSerializerSettingsForJsonApi(settings); + + settings.Formatting = Formatting.Indented; + } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs new file mode 100644 index 0000000000..1abb4e6a32 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs @@ -0,0 +1,8 @@ +using JsonApiDotNetCore.OpenApiClient; + +namespace JsonApiDotNetCoreExampleClient.GeneratedCode +{ + partial interface IExampleApiClient : IJsonApiClient + { + } +} diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj new file mode 100644 index 0000000000..c9eda8ee8a --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj @@ -0,0 +1,31 @@ + + + Exe + $(NetCoreAppVersion) + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + ExampleApiClient + NSwagCSharp + JsonApiDotNetCoreExampleClient.GeneratedCode + /UseBaseUrl:false /GenerateClientInterfaces:true + + + + + + + diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs new file mode 100644 index 0000000000..db56a2ac9a --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs @@ -0,0 +1,35 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using JsonApiDotNetCoreExampleClient.GeneratedCode; + +#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException + +namespace JsonApiDotNetCoreExampleClient +{ + internal class Program + { + private static async Task Main() + { + using var httpClient = new HttpClient + { + BaseAddress = new Uri("http://localhost:14140") + }; + + IExampleApiClient exampleApiClient = new ExampleApiClient(httpClient); + + try + { + const int nonExistingId = int.MaxValue; + await exampleApiClient.Delete_personAsync(nonExistingId); + } + catch (ApiException exception) + { + Console.WriteLine(exception.Response); + } + + Console.WriteLine("Press any key to close."); + Console.ReadKey(); + } + } +} diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/swagger.json b/src/Examples/JsonApiDotNetCoreExampleClient/swagger.json new file mode 100644 index 0000000000..bb8de0e115 --- /dev/null +++ b/src/Examples/JsonApiDotNetCoreExampleClient/swagger.json @@ -0,0 +1,2653 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "JsonApiDotNetCoreExample", + "version": "1.0" + }, + "paths": { + "/api/v1/people": { + "get": { + "tags": [ + "people" + ], + "operationId": "get person Collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/person-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "people" + ], + "operationId": "head person Collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/person-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "people" + ], + "operationId": "post person", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/person-post-request-document" + } + } + } + }, + "responses": { + "201": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/person-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/people/{id}": { + "get": { + "tags": [ + "people" + ], + "operationId": "get person", + "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/person-primary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "people" + ], + "operationId": "head person", + "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/person-primary-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "people" + ], + "operationId": "patch person", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/person-patch-request-document" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/person-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "people" + ], + "operationId": "delete person", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/people/{id}/assignedTodoItems": { + "get": { + "tags": [ + "people" + ], + "operationId": "get person assignedTodoItems", + "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/todoItem-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "people" + ], + "operationId": "head person assignedTodoItems", + "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/todoItem-collection-response-document" + } + } + } + } + } + } + }, + "/api/v1/people/{id}/relationships/assignedTodoItems": { + "get": { + "tags": [ + "people" + ], + "operationId": "get person assignedTodoItems Relationship", + "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/todoItem-identifier-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "people" + ], + "operationId": "head person assignedTodoItems Relationship", + "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/todoItem-identifier-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "people" + ], + "operationId": "post person assignedTodoItems Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "people" + ], + "operationId": "patch person assignedTodoItems Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "people" + ], + "operationId": "delete person assignedTodoItems Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/tags": { + "get": { + "tags": [ + "tags" + ], + "operationId": "get tag Collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/tag-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "tags" + ], + "operationId": "head tag Collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/tag-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "tags" + ], + "operationId": "post tag", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/tag-post-request-document" + } + } + } + }, + "responses": { + "201": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/tag-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/tags/{id}": { + "get": { + "tags": [ + "tags" + ], + "operationId": "get tag", + "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/tag-primary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "tags" + ], + "operationId": "head tag", + "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/tag-primary-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "tags" + ], + "operationId": "patch tag", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/tag-patch-request-document" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/tag-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "tags" + ], + "operationId": "delete tag", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/tags/{id}/todoItems": { + "get": { + "tags": [ + "tags" + ], + "operationId": "get tag todoItems", + "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/todoItem-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "tags" + ], + "operationId": "head tag todoItems", + "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/todoItem-collection-response-document" + } + } + } + } + } + } + }, + "/api/v1/tags/{id}/relationships/todoItems": { + "get": { + "tags": [ + "tags" + ], + "operationId": "get tag todoItems Relationship", + "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/todoItem-identifier-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "tags" + ], + "operationId": "head tag todoItems Relationship", + "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/todoItem-identifier-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "tags" + ], + "operationId": "post tag todoItems Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "tags" + ], + "operationId": "patch tag todoItems Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "tags" + ], + "operationId": "delete tag todoItems Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/todoItems": { + "get": { + "tags": [ + "todoItems" + ], + "operationId": "get todoItem Collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/todoItem-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "todoItems" + ], + "operationId": "head todoItem Collection", + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/todoItem-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "todoItems" + ], + "operationId": "post todoItem", + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/todoItem-post-request-document" + } + } + } + }, + "responses": { + "201": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/todoItem-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/todoItems/{id}": { + "get": { + "tags": [ + "todoItems" + ], + "operationId": "get todoItem", + "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/todoItem-primary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "todoItems" + ], + "operationId": "head todoItem", + "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/todoItem-primary-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "todoItems" + ], + "operationId": "patch todoItem", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/todoItem-patch-request-document" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/todoItem-primary-response-document" + } + } + } + }, + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "todoItems" + ], + "operationId": "delete todoItem", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/todoItems/{id}/assignee": { + "get": { + "tags": [ + "todoItems" + ], + "operationId": "get todoItem assignee", + "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/person-secondary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "todoItems" + ], + "operationId": "head todoItem assignee", + "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/person-secondary-response-document" + } + } + } + } + } + } + }, + "/api/v1/todoItems/{id}/relationships/assignee": { + "get": { + "tags": [ + "todoItems" + ], + "operationId": "get todoItem assignee Relationship", + "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/person-identifier-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "todoItems" + ], + "operationId": "head todoItem assignee Relationship", + "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/person-identifier-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "todoItems" + ], + "operationId": "patch todoItem assignee Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-one-person-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/todoItems/{id}/owner": { + "get": { + "tags": [ + "todoItems" + ], + "operationId": "get todoItem owner", + "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/person-secondary-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "todoItems" + ], + "operationId": "head todoItem owner", + "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/person-secondary-response-document" + } + } + } + } + } + } + }, + "/api/v1/todoItems/{id}/relationships/owner": { + "get": { + "tags": [ + "todoItems" + ], + "operationId": "get todoItem owner Relationship", + "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/person-identifier-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "todoItems" + ], + "operationId": "head todoItem owner Relationship", + "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/person-identifier-response-document" + } + } + } + } + } + }, + "patch": { + "tags": [ + "todoItems" + ], + "operationId": "patch todoItem owner Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-one-person-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + }, + "/api/v1/todoItems/{id}/tags": { + "get": { + "tags": [ + "todoItems" + ], + "operationId": "get todoItem tags", + "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/tag-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "todoItems" + ], + "operationId": "head todoItem tags", + "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/tag-collection-response-document" + } + } + } + } + } + } + }, + "/api/v1/todoItems/{id}/relationships/tags": { + "get": { + "tags": [ + "todoItems" + ], + "operationId": "get todoItem tags Relationship", + "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/tag-identifier-collection-response-document" + } + } + } + } + } + }, + "head": { + "tags": [ + "todoItems" + ], + "operationId": "head todoItem tags Relationship", + "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/tag-identifier-collection-response-document" + } + } + } + } + } + }, + "post": { + "tags": [ + "todoItems" + ], + "operationId": "post todoItem tags Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-tag-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "patch": { + "tags": [ + "todoItems" + ], + "operationId": "patch todoItem tags Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-tag-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "todoItems" + ], + "operationId": "delete todoItem tags Relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-tag-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "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": { + "type": "string" + }, + "related": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceCollectionDocument": { + "required": [ + "first", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceDocument": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierCollectionDocument": { + "required": [ + "first", + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "type": "string" + }, + "first": { + "type": "string" + }, + "last": { + "type": "string" + }, + "prev": { + "type": "string" + }, + "next": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceIdentifierDocument": { + "required": [ + "related", + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + }, + "describedby": { + "type": "string" + }, + "related": { + "type": "string" + } + }, + "additionalProperties": false + }, + "linksInResourceObject": { + "required": [ + "self" + ], + "type": "object", + "properties": { + "self": { + "type": "string" + } + }, + "additionalProperties": false + }, + "null-value": { + "not": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "object" + }, + { + "type": "array" + } + ], + "items": { } + }, + "nullable": true + }, + "people-resource-type": { + "enum": [ + "people" + ], + "type": "string" + }, + "person-attributes-in-patch-request": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "person-attributes-in-post-request": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "person-attributes-in-response": { + "type": "object", + "properties": { + "firstName": { + "type": "string", + "nullable": true + }, + "lastName": { + "type": "string", + "nullable": true + } + }, + "additionalProperties": false + }, + "person-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/person-data-in-response" + } + } + }, + "additionalProperties": false + }, + "person-data-in-patch-request": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/people-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/person-attributes-in-patch-request" + }, + "relationships": { + "$ref": "#/components/schemas/person-relationships-in-patch-request" + } + }, + "additionalProperties": false + }, + "person-data-in-post-request": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/people-resource-type" + }, + "attributes": { + "$ref": "#/components/schemas/person-attributes-in-post-request" + }, + "relationships": { + "$ref": "#/components/schemas/person-relationships-in-post-request" + } + }, + "additionalProperties": false + }, + "person-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/people-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/person-attributes-in-response" + }, + "relationships": { + "$ref": "#/components/schemas/person-relationships-in-response" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "person-identifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/people-resource-type" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "person-identifier-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierDocument" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/person-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "person-patch-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/person-data-in-patch-request" + } + }, + "additionalProperties": false + }, + "person-post-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/person-data-in-post-request" + } + }, + "additionalProperties": false + }, + "person-primary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + }, + "data": { + "$ref": "#/components/schemas/person-data-in-response" + } + }, + "additionalProperties": false + }, + "person-relationships-in-patch-request": { + "type": "object", + "properties": { + "assignedTodoItems": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + }, + "additionalProperties": false + }, + "person-relationships-in-post-request": { + "type": "object", + "properties": { + "assignedTodoItems": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + }, + "additionalProperties": false + }, + "person-relationships-in-response": { + "type": "object", + "properties": { + "assignedTodoItems": { + "$ref": "#/components/schemas/to-many-todoItem-response-data" + } + }, + "additionalProperties": false + }, + "person-secondary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/person-data-in-response" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "tag-attributes-in-patch-request": { + "type": "object", + "properties": { + "name": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "tag-attributes-in-post-request": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "tag-attributes-in-response": { + "type": "object", + "properties": { + "name": { + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + }, + "tag-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/tag-data-in-response" + } + } + }, + "additionalProperties": false + }, + "tag-data-in-patch-request": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/tags-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/tag-attributes-in-patch-request" + }, + "relationships": { + "$ref": "#/components/schemas/tag-relationships-in-patch-request" + } + }, + "additionalProperties": false + }, + "tag-data-in-post-request": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/tags-resource-type" + }, + "attributes": { + "$ref": "#/components/schemas/tag-attributes-in-post-request" + }, + "relationships": { + "$ref": "#/components/schemas/tag-relationships-in-post-request" + } + }, + "additionalProperties": false + }, + "tag-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/tags-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/tag-attributes-in-response" + }, + "relationships": { + "$ref": "#/components/schemas/tag-relationships-in-response" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "tag-identifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/tags-resource-type" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "tag-identifier-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/tag-identifier" + } + } + }, + "additionalProperties": false + }, + "tag-patch-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/tag-data-in-patch-request" + } + }, + "additionalProperties": false + }, + "tag-post-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/tag-data-in-post-request" + } + }, + "additionalProperties": false + }, + "tag-primary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + }, + "data": { + "$ref": "#/components/schemas/tag-data-in-response" + } + }, + "additionalProperties": false + }, + "tag-relationships-in-patch-request": { + "type": "object", + "properties": { + "todoItems": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + }, + "additionalProperties": false + }, + "tag-relationships-in-post-request": { + "type": "object", + "properties": { + "todoItems": { + "$ref": "#/components/schemas/to-many-todoItem-request-data" + } + }, + "additionalProperties": false + }, + "tag-relationships-in-response": { + "type": "object", + "properties": { + "todoItems": { + "$ref": "#/components/schemas/to-many-todoItem-response-data" + } + }, + "additionalProperties": false + }, + "tags-resource-type": { + "enum": [ + "tags" + ], + "type": "string" + }, + "to-many-tag-request-data": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/tag-identifier" + } + } + }, + "additionalProperties": false + }, + "to-many-tag-response-data": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/tag-identifier" + } + } + }, + "additionalProperties": false + }, + "to-many-todoItem-request-data": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/todoItem-identifier" + } + } + }, + "additionalProperties": false + }, + "to-many-todoItem-response-data": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/todoItem-identifier" + } + } + }, + "additionalProperties": false + }, + "to-one-person-request-data": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/person-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "to-one-person-response-data": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "$ref": "#/components/schemas/linksInRelationshipObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/person-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, + "todoItem-attributes-in-patch-request": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "priority": { + "$ref": "#/components/schemas/todoItemPriority" + } + }, + "additionalProperties": false + }, + "todoItem-attributes-in-post-request": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "priority": { + "$ref": "#/components/schemas/todoItemPriority" + } + }, + "additionalProperties": false + }, + "todoItem-attributes-in-response": { + "type": "object", + "properties": { + "description": { + "type": "string", + "nullable": true + }, + "priority": { + "$ref": "#/components/schemas/todoItemPriority" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "modifiedAt": { + "type": "string", + "format": "date-time", + "nullable": true + } + }, + "additionalProperties": false + }, + "todoItem-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceCollectionDocument" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/todoItem-data-in-response" + } + } + }, + "additionalProperties": false + }, + "todoItem-data-in-patch-request": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/todoItems-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/todoItem-attributes-in-patch-request" + }, + "relationships": { + "$ref": "#/components/schemas/todoItem-relationships-in-patch-request" + } + }, + "additionalProperties": false + }, + "todoItem-data-in-post-request": { + "required": [ + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/todoItems-resource-type" + }, + "attributes": { + "$ref": "#/components/schemas/todoItem-attributes-in-post-request" + }, + "relationships": { + "$ref": "#/components/schemas/todoItem-relationships-in-post-request" + } + }, + "additionalProperties": false + }, + "todoItem-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/todoItems-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/todoItem-attributes-in-response" + }, + "relationships": { + "$ref": "#/components/schemas/todoItem-relationships-in-response" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceObject" + }, + "meta": { + "type": "object", + "additionalProperties": { } + } + }, + "additionalProperties": false + }, + "todoItem-identifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/todoItems-resource-type" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "todoItem-identifier-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceIdentifierCollectionDocument" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/todoItem-identifier" + } + } + }, + "additionalProperties": false + }, + "todoItem-patch-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/todoItem-data-in-patch-request" + } + }, + "additionalProperties": false + }, + "todoItem-post-request-document": { + "required": [ + "data" + ], + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/todoItem-data-in-post-request" + } + }, + "additionalProperties": false + }, + "todoItem-primary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": { } + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapiObject" + }, + "links": { + "$ref": "#/components/schemas/linksInResourceDocument" + }, + "data": { + "$ref": "#/components/schemas/todoItem-data-in-response" + } + }, + "additionalProperties": false + }, + "todoItem-relationships-in-patch-request": { + "type": "object", + "properties": { + "owner": { + "$ref": "#/components/schemas/to-one-person-request-data" + }, + "assignee": { + "$ref": "#/components/schemas/to-one-person-request-data" + }, + "tags": { + "$ref": "#/components/schemas/to-many-tag-request-data" + } + }, + "additionalProperties": false + }, + "todoItem-relationships-in-post-request": { + "type": "object", + "properties": { + "owner": { + "$ref": "#/components/schemas/to-one-person-request-data" + }, + "assignee": { + "$ref": "#/components/schemas/to-one-person-request-data" + }, + "tags": { + "$ref": "#/components/schemas/to-many-tag-request-data" + } + }, + "additionalProperties": false + }, + "todoItem-relationships-in-response": { + "type": "object", + "properties": { + "owner": { + "$ref": "#/components/schemas/to-one-person-response-data" + }, + "assignee": { + "$ref": "#/components/schemas/to-one-person-response-data" + }, + "tags": { + "$ref": "#/components/schemas/to-many-tag-response-data" + } + }, + "additionalProperties": false + }, + "todoItemPriority": { + "enum": [ + "Low", + "Medium", + "High" + ], + "type": "string" + }, + "todoItems-resource-type": { + "enum": [ + "todoItems" + ], + "type": "string" + } + } + } +} From 7b3d1ef402d81f116a8e63ded4b6dd3347a48f23 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 15 Sep 2021 09:10:32 +0200 Subject: [PATCH 13/35] Fixed inspect and cleanup code issues that do not appear when executed through Rider --- .../GeneratedCode/ExampleApiClient.cs | 1 + .../GeneratedCode/IExampleApiClient.cs | 1 + .../JsonApiDotNetCoreExampleClient/Program.cs | 1 + .../swagger.json | 36 +++++++++---------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs index c9dd703249..39aaf35e5e 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs @@ -3,6 +3,7 @@ namespace JsonApiDotNetCoreExampleClient.GeneratedCode { + // ReSharper disable once MemberCanBeInternal public partial class ExampleApiClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs index 1abb4e6a32..ce663e842e 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs @@ -2,6 +2,7 @@ namespace JsonApiDotNetCoreExampleClient.GeneratedCode { + // ReSharper disable once MemberCanBeInternal partial interface IExampleApiClient : IJsonApiClient { } diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs index db56a2ac9a..4f68860910 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs @@ -7,6 +7,7 @@ namespace JsonApiDotNetCoreExampleClient { + // ReSharper disable once ClassNeverInstantiated.Global internal class Program { private static async Task Main() diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/swagger.json b/src/Examples/JsonApiDotNetCoreExampleClient/swagger.json index bb8de0e115..9a72c99145 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/swagger.json +++ b/src/Examples/JsonApiDotNetCoreExampleClient/swagger.json @@ -1542,7 +1542,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1688,7 +1688,7 @@ "type": "array" } ], - "items": { } + "items": {} }, "nullable": true }, @@ -1749,7 +1749,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -1831,7 +1831,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -1861,7 +1861,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -1915,7 +1915,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -1965,7 +1965,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -2028,7 +2028,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -2110,7 +2110,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -2140,7 +2140,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -2190,7 +2190,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -2263,7 +2263,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "data": { "type": "array", @@ -2300,7 +2300,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "data": { "type": "array", @@ -2341,7 +2341,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "data": { "oneOf": [ @@ -2413,7 +2413,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -2495,7 +2495,7 @@ }, "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} } }, "additionalProperties": false @@ -2525,7 +2525,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" @@ -2575,7 +2575,7 @@ "properties": { "meta": { "type": "object", - "additionalProperties": { } + "additionalProperties": {} }, "jsonapi": { "$ref": "#/components/schemas/jsonapiObject" From ba42b7708a115b40f8a3620203890a997448ce88 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 15 Sep 2021 14:53:35 +0200 Subject: [PATCH 14/35] rename existing to legacy in ExistingOpenApiIntegration folder and namespace --- .../AircraftType.cs | 2 +- .../Airline.cs | 2 +- .../Airplane.cs | 2 +- .../AirplanesController.cs | 2 +- .../Flight.cs | 2 +- .../FlightAttendant.cs | 2 +- .../FlightAttendantExpertiseLevel.cs | 2 +- .../FlightAttendantsController.cs | 2 +- .../FlightsController.cs | 2 +- .../LegacyIntegrationDbContext.cs} | 6 +++--- .../LegacyOpenApiIntegrationStartup.cs} | 4 ++-- .../LegacyOpenApiIntegrationTests.cs} | 10 +++++----- .../swagger.json | 0 test/OpenApiTests/OpenApiTests.csproj | 4 ++-- 14 files changed, 21 insertions(+), 21 deletions(-) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/AircraftType.cs (80%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/Airline.cs (79%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/Airplane.cs (97%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/AirplanesController.cs (90%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/Flight.cs (96%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/FlightAttendant.cs (96%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/FlightAttendantExpertiseLevel.cs (81%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/FlightAttendantsController.cs (90%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/FlightsController.cs (89%) rename test/OpenApiTests/{ExistingOpenApiIntegration/ExistingIntegrationDbContext.cs => LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs} (82%) rename test/OpenApiTests/{ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs => LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs} (87%) rename test/OpenApiTests/{ExistingOpenApiIntegration/ExistingOpenApiIntegrationTests.cs => LegacyOpenApiIntegration/LegacyOpenApiIntegrationTests.cs} (84%) rename test/OpenApiTests/{ExistingOpenApiIntegration => LegacyOpenApiIntegration}/swagger.json (100%) diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/AircraftType.cs b/test/OpenApiTests/LegacyOpenApiIntegration/AircraftType.cs similarity index 80% rename from test/OpenApiTests/ExistingOpenApiIntegration/AircraftType.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/AircraftType.cs index 56da2f05b3..617a8a2a93 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/AircraftType.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/AircraftType.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum AircraftType diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/Airline.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Airline.cs similarity index 79% rename from test/OpenApiTests/ExistingOpenApiIntegration/Airline.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/Airline.cs index 5eb49adec4..7c0725a149 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/Airline.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Airline.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum Airline diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/Airplane.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs similarity index 97% rename from test/OpenApiTests/ExistingOpenApiIntegration/Airplane.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs index 98a21686b5..efb9efb977 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/Airplane.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs @@ -5,7 +5,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Airplane : Identifiable diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/AirplanesController.cs b/test/OpenApiTests/LegacyOpenApiIntegration/AirplanesController.cs similarity index 90% rename from test/OpenApiTests/ExistingOpenApiIntegration/AirplanesController.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/AirplanesController.cs index 9666bba7db..3289ded0d4 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/AirplanesController.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/AirplanesController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { public sealed class AirplanesController : JsonApiController { diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/Flight.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs similarity index 96% rename from test/OpenApiTests/ExistingOpenApiIntegration/Flight.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs index 84b8bc2908..f948110a41 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/Flight.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs @@ -5,7 +5,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class Flight : Identifiable diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendant.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs similarity index 96% rename from test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendant.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs index 7698c62912..6e19636045 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendant.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs @@ -4,7 +4,7 @@ using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class FlightAttendant : Identifiable diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantExpertiseLevel.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantExpertiseLevel.cs similarity index 81% rename from test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantExpertiseLevel.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantExpertiseLevel.cs index fe23c1e4b5..c5ffe7cb49 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantExpertiseLevel.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantExpertiseLevel.cs @@ -1,6 +1,6 @@ using JetBrains.Annotations; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] public enum FlightAttendantExpertiseLevel diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantsController.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs similarity index 90% rename from test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantsController.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs index d3c38a1d67..43adc8d0eb 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/FlightAttendantsController.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { public sealed class FlightAttendantsController : JsonApiController { diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/FlightsController.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightsController.cs similarity index 89% rename from test/OpenApiTests/ExistingOpenApiIntegration/FlightsController.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/FlightsController.cs index 74bff6612d..0b764d052f 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/FlightsController.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightsController.cs @@ -3,7 +3,7 @@ using JsonApiDotNetCore.Services; using Microsoft.Extensions.Logging; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { public sealed class FlightsController : JsonApiController { diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingIntegrationDbContext.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs similarity index 82% rename from test/OpenApiTests/ExistingOpenApiIntegration/ExistingIntegrationDbContext.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs index ef534022ab..ff022c1879 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingIntegrationDbContext.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs @@ -4,16 +4,16 @@ // @formatter:wrap_chained_method_calls chop_always // @formatter:keep_existing_linebreaks true -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class ExistingIntegrationDbContext : DbContext + public sealed class LegacyIntegrationDbContext : DbContext { public DbSet Airplanes { get; set; } public DbSet Flights { get; set; } public DbSet FlightAttendants { get; set; } - public ExistingIntegrationDbContext(DbContextOptions options) + public LegacyIntegrationDbContext(DbContextOptions options) : base(options) { } diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs similarity index 87% rename from test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs index ec13d3e12e..85dfd3d1e8 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationStartup.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs @@ -5,10 +5,10 @@ using Newtonsoft.Json.Serialization; using OpenApiTests.Startups; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] - public sealed class ExistingOpenApiIntegrationStartup : OpenApiStartup + public sealed class LegacyOpenApiIntegrationStartup : OpenApiStartup where TDbContext : DbContext { protected override void SetJsonApiOptions(JsonApiOptions options) diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationTests.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationTests.cs similarity index 84% rename from test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationTests.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationTests.cs index b23398e901..313c78aae3 100644 --- a/test/OpenApiTests/ExistingOpenApiIntegration/ExistingOpenApiIntegrationTests.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationTests.cs @@ -7,12 +7,12 @@ using TestBuildingBlocks; using Xunit; -namespace OpenApiTests.ExistingOpenApiIntegration +namespace OpenApiTests.LegacyOpenApiIntegration { - public sealed class ExistingOpenApiIntegrationTests - : IntegrationTestContext, ExistingIntegrationDbContext> + public sealed class LegacyOpenApiIntegrationTests + : IntegrationTestContext, LegacyIntegrationDbContext> { - public ExistingOpenApiIntegrationTests() + public LegacyOpenApiIntegrationTests() { UseController(); UseController(); @@ -23,7 +23,7 @@ public ExistingOpenApiIntegrationTests() public async Task Retrieved_document_matches_expected_document() { // Arrange - string embeddedResourceName = $"{nameof(OpenApiTests)}.{nameof(ExistingOpenApiIntegration)}.swagger.json"; + string embeddedResourceName = $"{nameof(OpenApiTests)}.{nameof(LegacyOpenApiIntegration)}.swagger.json"; string expectedDocument = await LoadEmbeddedResourceAsync(embeddedResourceName); const string requestUrl = "swagger/v1/swagger.json"; diff --git a/test/OpenApiTests/ExistingOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json similarity index 100% rename from test/OpenApiTests/ExistingOpenApiIntegration/swagger.json rename to test/OpenApiTests/LegacyOpenApiIntegration/swagger.json diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index fced745af1..26a1dbc9bb 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -29,7 +29,7 @@ - + OpenApiTests.ClientLibrary.GeneratedCode OpenApiClient NSwagCSharp @@ -38,7 +38,7 @@ - + From ecd971eb2703ba8b3b136fd4e59301a7002987c9 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 15 Sep 2021 15:34:18 +0200 Subject: [PATCH 15/35] moved OpenApiClient to OpenApi.Client --- JsonApiDotNetCore.sln | 2 +- docs/usage/toc.md | 2 +- .../GeneratedCode/ExampleApiClient.cs | 2 +- .../GeneratedCode/IExampleApiClient.cs | 2 +- .../JsonApiDotNetCoreExampleClient.csproj | 2 +- .../ArgumentGuard.cs | 21 +++++++++++++++++++ .../IJsonApiClient.cs | 2 +- .../JsonApiClient.cs | 7 +++---- .../JsonApiDotNetCore.OpenApi.Client.csproj} | 7 +------ .../GeneratedCode/IOpenApiClient.cs | 2 +- .../GeneratedCode/OpenApiClient.cs | 2 +- test/OpenApiTests/OpenApiTests.csproj | 1 + 12 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 src/JsonApiDotNetCore.OpenApi.Client/ArgumentGuard.cs rename src/{JsonApiDotNetCore.OpenApiClient => JsonApiDotNetCore.OpenApi.Client}/IJsonApiClient.cs (98%) rename src/{JsonApiDotNetCore.OpenApiClient => JsonApiDotNetCore.OpenApi.Client}/JsonApiClient.cs (98%) rename src/{JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj => JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj} (81%) diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln index e282f88712..e0be4f2d78 100644 --- a/JsonApiDotNetCore.sln +++ b/JsonApiDotNetCore.sln @@ -48,7 +48,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiTests", "test\OpenApiTests\OpenApiTests.csproj", "{B693DE14-BB28-496F-AB39-B4E674ABCA80}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApiClient", "src\JsonApiDotNetCore.OpenApiClient\JsonApiDotNetCore.OpenApiClient.csproj", "{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi.Client", "src\JsonApiDotNetCore.OpenApi.Client\JsonApiDotNetCore.OpenApi.Client.csproj", "{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreExampleClient", "src\Examples\JsonApiDotNetCoreExampleClient\JsonApiDotNetCoreExampleClient.csproj", "{7FC5DFA3-6F66-4FD8-820D-81E93856F252}" EndProject diff --git a/docs/usage/toc.md b/docs/usage/toc.md index fb881c7a58..c3ce5513f2 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -24,7 +24,7 @@ # OpenAPI ## [API Description](openapi/api-description.md)) -## [Client Library](openapi/client-library.md)) +### [Client Library](openapi/client-library.md)) # Extensibility ## [Layer Overview](extensibility/layer-overview.md) diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs index 39aaf35e5e..8f7902def1 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.OpenApiClient; +using JsonApiDotNetCore.OpenApi.Client; using Newtonsoft.Json; namespace JsonApiDotNetCoreExampleClient.GeneratedCode diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs index ce663e842e..3519c54284 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.OpenApiClient; +using JsonApiDotNetCore.OpenApi.Client; namespace JsonApiDotNetCoreExampleClient.GeneratedCode { diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj index c9eda8ee8a..d938bebbe4 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj +++ b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj @@ -26,6 +26,6 @@ - + diff --git a/src/JsonApiDotNetCore.OpenApi.Client/ArgumentGuard.cs b/src/JsonApiDotNetCore.OpenApi.Client/ArgumentGuard.cs new file mode 100644 index 0000000000..6feadb9a00 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client/ArgumentGuard.cs @@ -0,0 +1,21 @@ +using System; +using JetBrains.Annotations; + +#pragma warning disable AV1008 // Class should not be static + +namespace JsonApiDotNetCore.OpenApi.Client +{ + internal static class ArgumentGuard + { + [AssertionMethod] + [ContractAnnotation("value: null => halt")] + public static void NotNull([CanBeNull] [NoEnumeration] T value, [NotNull] [InvokerParameterName] string name) + where T : class + { + if (value is null) + { + throw new ArgumentNullException(name); + } + } + } +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.OpenApiClient/IJsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs similarity index 98% rename from src/JsonApiDotNetCore.OpenApiClient/IJsonApiClient.cs rename to src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs index 39eabca632..1a78e31242 100644 --- a/src/JsonApiDotNetCore.OpenApiClient/IJsonApiClient.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/IJsonApiClient.cs @@ -1,7 +1,7 @@ using System; using System.Linq.Expressions; -namespace JsonApiDotNetCore.OpenApiClient +namespace JsonApiDotNetCore.OpenApi.Client { public interface IJsonApiClient { diff --git a/src/JsonApiDotNetCore.OpenApiClient/JsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs similarity index 98% rename from src/JsonApiDotNetCore.OpenApiClient/JsonApiClient.cs rename to src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs index 39112e98dd..55a9e73176 100644 --- a/src/JsonApiDotNetCore.OpenApiClient/JsonApiClient.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs @@ -4,11 +4,10 @@ using System.Linq.Expressions; using System.Reflection; using JetBrains.Annotations; -using JsonApiDotNetCore.OpenApi; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -namespace JsonApiDotNetCore.OpenApiClient +namespace JsonApiDotNetCore.OpenApi.Client { /// /// Base class to inherit auto-generated client from. Enables to mark fields to be explicitly included in a request body, even if they are null or @@ -116,7 +115,7 @@ public override bool CanConvert(Type objectType) public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - throw new UnreachableCodeException(); + throw new Exception("This code should not be reachable."); } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) @@ -221,4 +220,4 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ } } } -} +} \ No newline at end of file diff --git a/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj similarity index 81% rename from src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj rename to src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj index 5ac19d01d4..f7dbcd4b59 100644 --- a/src/JsonApiDotNetCore.OpenApiClient/JsonApiDotNetCore.OpenApiClient.csproj +++ b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj @@ -7,7 +7,7 @@ jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net;openapi;swagger;client;nswag - Contains utility methods to enrich the usability of an OpenAPI generated client. + Provides support for partial PATCH and partial POST requests for a C# client generated with NSwag. json-api-dotnet https://www.jsonapi.net/ MIT @@ -17,11 +17,6 @@ embedded - - - - - all diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs b/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs index 3ff3230060..399e93b663 100644 --- a/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs +++ b/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.OpenApiClient; +using JsonApiDotNetCore.OpenApi.Client; namespace OpenApiTests.ClientLibrary.GeneratedCode { diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs b/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs index 411250efc6..3602570f73 100644 --- a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs +++ b/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs @@ -1,4 +1,4 @@ -using JsonApiDotNetCore.OpenApiClient; +using JsonApiDotNetCore.OpenApi.Client; using Newtonsoft.Json; namespace OpenApiTests.ClientLibrary.GeneratedCode diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index 26a1dbc9bb..bc0fe27152 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -4,6 +4,7 @@ + From 37a95b378c371f4d34bdd277f7ef762585df88ab Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 15 Sep 2021 16:10:44 +0200 Subject: [PATCH 16/35] Configure NSwag to generate a client with access modifier set to internal. Simplified ExampleApiClient to not use a generated interface --- .../GeneratedCode/ExampleApiClient.cs | 3 +-- .../GeneratedCode/IExampleApiClient.cs | 9 --------- .../JsonApiDotNetCoreExampleClient.csproj | 2 +- src/Examples/JsonApiDotNetCoreExampleClient/Program.cs | 2 +- .../ClientLibrary/GeneratedCode/OpenApiClient.cs | 3 +-- test/OpenApiTests/OpenApiTests.csproj | 3 +-- 6 files changed, 5 insertions(+), 17 deletions(-) delete mode 100644 src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs index 8f7902def1..02950157e7 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs @@ -3,8 +3,7 @@ namespace JsonApiDotNetCoreExampleClient.GeneratedCode { - // ReSharper disable once MemberCanBeInternal - public partial class ExampleApiClient : JsonApiClient + internal partial class ExampleApiClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) { diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs deleted file mode 100644 index 3519c54284..0000000000 --- a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/IExampleApiClient.cs +++ /dev/null @@ -1,9 +0,0 @@ -using JsonApiDotNetCore.OpenApi.Client; - -namespace JsonApiDotNetCoreExampleClient.GeneratedCode -{ - // ReSharper disable once MemberCanBeInternal - partial interface IExampleApiClient : IJsonApiClient - { - } -} diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj index d938bebbe4..79544c38cf 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj +++ b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj @@ -21,7 +21,7 @@ ExampleApiClient NSwagCSharp JsonApiDotNetCoreExampleClient.GeneratedCode - /UseBaseUrl:false /GenerateClientInterfaces:true + /UseBaseUrl:false /ClientClassAccessModifier:internal diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs index 4f68860910..1370f4f4f6 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs @@ -17,7 +17,7 @@ private static async Task Main() BaseAddress = new Uri("http://localhost:14140") }; - IExampleApiClient exampleApiClient = new ExampleApiClient(httpClient); + ExampleApiClient exampleApiClient = new(httpClient); try { diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs b/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs index 3602570f73..d72102f33a 100644 --- a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs +++ b/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs @@ -3,8 +3,7 @@ namespace OpenApiTests.ClientLibrary.GeneratedCode { - // ReSharper disable once MemberCanBeInternal - public partial class OpenApiClient : JsonApiClient + internal partial class OpenApiClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) { diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index bc0fe27152..964ff0ab7d 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -5,7 +5,6 @@ - @@ -34,7 +33,7 @@ OpenApiTests.ClientLibrary.GeneratedCode OpenApiClient NSwagCSharp - /UseBaseUrl:false /GenerateClientInterfaces:true + /UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal From da1db37b6cbd6c09d732c5accfc36b663639edd4 Mon Sep 17 00:00:00 2001 From: maurei Date: Wed, 15 Sep 2021 22:34:47 +0200 Subject: [PATCH 17/35] Move OpenAPI client tests to dedicated test project, cleared up some unrequired internalsVisibleTo usage --- JsonApiDotNetCore.sln | 15 ++++++ .../JsonApiDotNetCoreExampleClient/Program.cs | 2 - .../AssemblyInfo.cs | 3 ++ src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs | 3 -- .../LegacyClient}/ApiResponse.cs | 6 +-- ...lientAttributeRegistrationLifeTimeTests.cs | 20 ++++---- .../GeneratedCode/ILegacyClient.cs | 9 ++++ .../GeneratedCode/LegacyClient.cs} | 4 +- .../LegacyClient}/RequestTests.cs | 32 ++++++------ .../LegacyClient}/ResponseTests.cs | 34 ++++++------- .../OpenApiClientTests.csproj | 49 +++++++++++++++++++ .../GeneratedCode/IOpenApiClient.cs | 9 ---- 12 files changed, 124 insertions(+), 62 deletions(-) create mode 100644 src/JsonApiDotNetCore.OpenApi.Client/AssemblyInfo.cs delete mode 100644 src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs rename test/{OpenApiTests/ClientLibrary => OpenApiClientTests/LegacyClient}/ApiResponse.cs (84%) rename test/{OpenApiTests/ClientLibrary => OpenApiClientTests/LegacyClient}/ClientAttributeRegistrationLifeTimeTests.cs (95%) create mode 100644 test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs rename test/{OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs => OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs} (73%) rename test/{OpenApiTests/ClientLibrary => OpenApiClientTests/LegacyClient}/RequestTests.cs (92%) rename test/{OpenApiTests/ClientLibrary => OpenApiClientTests/LegacyClient}/ResponseTests.cs (94%) create mode 100644 test/OpenApiClientTests/OpenApiClientTests.csproj delete mode 100644 test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs diff --git a/JsonApiDotNetCore.sln b/JsonApiDotNetCore.sln index e0be4f2d78..311a1a586c 100644 --- a/JsonApiDotNetCore.sln +++ b/JsonApiDotNetCore.sln @@ -52,6 +52,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi.C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreExampleClient", "src\Examples\JsonApiDotNetCoreExampleClient\JsonApiDotNetCoreExampleClient.csproj", "{7FC5DFA3-6F66-4FD8-820D-81E93856F252}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiClientTests", "test\OpenApiClientTests\OpenApiClientTests.csproj", "{77F98215-3085-422E-B99D-4C404C2114CF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -266,6 +268,18 @@ Global {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x64.Build.0 = Release|Any CPU {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x86.ActiveCfg = Release|Any CPU {7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x86.Build.0 = Release|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Debug|x64.ActiveCfg = Debug|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Debug|x64.Build.0 = Debug|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Debug|x86.ActiveCfg = Debug|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Debug|x86.Build.0 = Debug|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Release|Any CPU.Build.0 = Release|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Release|x64.ActiveCfg = Release|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Release|x64.Build.0 = Release|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Release|x86.ActiveCfg = Release|Any CPU + {77F98215-3085-422E-B99D-4C404C2114CF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -288,6 +302,7 @@ Global {B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} {5ADAA902-5A75-4ECB-B4B4-03291D63CE9C} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF} {7FC5DFA3-6F66-4FD8-820D-81E93856F252} = {026FBC6C-AF76-4568-9B87-EC73457899FD} + {77F98215-3085-422E-B99D-4C404C2114CF} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4} diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs index 1370f4f4f6..bfdfdc469a 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs @@ -3,8 +3,6 @@ using System.Threading.Tasks; using JsonApiDotNetCoreExampleClient.GeneratedCode; -#pragma warning disable AV1210 // Catch a specific exception instead of Exception, SystemException or ApplicationException - namespace JsonApiDotNetCoreExampleClient { // ReSharper disable once ClassNeverInstantiated.Global diff --git a/src/JsonApiDotNetCore.OpenApi.Client/AssemblyInfo.cs b/src/JsonApiDotNetCore.OpenApi.Client/AssemblyInfo.cs new file mode 100644 index 0000000000..2871965d43 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi.Client/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("OpenApiClientTests")] diff --git a/src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs b/src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs deleted file mode 100644 index 9bb84b072a..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApiClient")] diff --git a/test/OpenApiTests/ClientLibrary/ApiResponse.cs b/test/OpenApiClientTests/LegacyClient/ApiResponse.cs similarity index 84% rename from test/OpenApiTests/ClientLibrary/ApiResponse.cs rename to test/OpenApiClientTests/LegacyClient/ApiResponse.cs index f2b1fbb293..73406fad09 100644 --- a/test/OpenApiTests/ClientLibrary/ApiResponse.cs +++ b/test/OpenApiClientTests/LegacyClient/ApiResponse.cs @@ -1,11 +1,11 @@ using System; using System.Threading.Tasks; -using JsonApiDotNetCore; -using OpenApiTests.ClientLibrary.GeneratedCode; +using JsonApiDotNetCore.OpenApi.Client; +using OpenApiClientTests.LegacyClient.GeneratedCode; #pragma warning disable AV1008 // Class should not be static -namespace OpenApiTests.ClientLibrary +namespace OpenApiClientTests.LegacyClient { internal static class ApiResponse { diff --git a/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs similarity index 95% rename from test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs rename to test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs index c5b7c06cb8..c876a2497d 100644 --- a/test/OpenApiTests/ClientLibrary/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs @@ -3,13 +3,13 @@ using FluentAssertions; using FluentAssertions.Common; using FluentAssertions.Extensions; -using OpenApiTests.ClientLibrary.GeneratedCode; +using OpenApiClientTests.LegacyClient.GeneratedCode; using TestBuildingBlocks; using Xunit; #pragma warning disable AV1704 // Don't include numbers in variables, parameters and type members -namespace OpenApiTests.ClientLibrary +namespace OpenApiClientTests.LegacyClient { public sealed class ClientAttributeRegistrationLifetimeTests { @@ -18,7 +18,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); @@ -64,7 +64,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r public async Task Attribute_registration_can_be_used_for_multiple_requests() { using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Arrange const string airplaneId = "XUuiP"; @@ -112,7 +112,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); @@ -171,7 +171,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ public async Task Attribute_values_can_be_changed_after_attribute_registration() { using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Arrange const string airplaneId = "XUuiP"; @@ -214,7 +214,7 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() public async Task Attribute_registration_is_unaffected_by_successive_attribute_registration_for_document_of_different_type() { using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Arrange const string airplaneId1 = "XUuiP"; @@ -265,7 +265,7 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r public async Task Attribute_registration_is_unaffected_by_preceding_disposed_attribute_registration_for_different_document_of_same_type() { using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Arrange const string airplaneId1 = "XUuiP"; @@ -327,7 +327,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att public async Task Attribute_registration_is_unaffected_by_preceding_disposed_attribute_registration_for_document_of_different_type() { using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Arrange var requestDocument1 = new AirplanePostRequestDocument @@ -386,7 +386,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att public async Task Attribute_registration_is_unaffected_by_preceding_attribute_registration_for_different_document_of_same_type() { using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Arrange const string airplaneId1 = "XUuiP"; diff --git a/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs b/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs new file mode 100644 index 0000000000..c3d70d1da3 --- /dev/null +++ b/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs @@ -0,0 +1,9 @@ +using JsonApiDotNetCore.OpenApi.Client; + +namespace OpenApiClientTests.LegacyClient.GeneratedCode +{ + // ReSharper disable once MemberCanBeInternal + partial interface ILegacyClient : IJsonApiClient + { + } +} diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs b/test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs similarity index 73% rename from test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs rename to test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs index d72102f33a..7d023412e3 100644 --- a/test/OpenApiTests/ClientLibrary/GeneratedCode/OpenApiClient.cs +++ b/test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs @@ -1,9 +1,9 @@ using JsonApiDotNetCore.OpenApi.Client; using Newtonsoft.Json; -namespace OpenApiTests.ClientLibrary.GeneratedCode +namespace OpenApiClientTests.LegacyClient.GeneratedCode { - internal partial class OpenApiClient : JsonApiClient + internal partial class LegacyClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) { diff --git a/test/OpenApiTests/ClientLibrary/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs similarity index 92% rename from test/OpenApiTests/ClientLibrary/RequestTests.cs rename to test/OpenApiClientTests/LegacyClient/RequestTests.cs index 4119b514dd..700138d8ba 100644 --- a/test/OpenApiTests/ClientLibrary/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -7,13 +7,13 @@ using FluentAssertions.Extensions; using JsonApiDotNetCore.Middleware; using Microsoft.Net.Http.Headers; -using OpenApiTests.ClientLibrary.GeneratedCode; +using OpenApiClientTests.LegacyClient.GeneratedCode; using TestBuildingBlocks; using Xunit; #pragma warning disable AV1500 // Member or local function contains too many statements -namespace OpenApiTests.ClientLibrary +namespace OpenApiClientTests.LegacyClient { public sealed class RequestTests { @@ -24,7 +24,7 @@ public async Task Getting_resource_collection_produces_expected_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCollectionAsync()); @@ -43,7 +43,7 @@ public async Task Getting_resource_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightAsync(flightId)); @@ -60,7 +60,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); var requestDocument = new FlightPostRequestDocument { @@ -102,7 +102,7 @@ public async Task Partial_posting_resource_produces_expected_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); const char euroSign = '\x20AC'; const char checkMark = '\x2713'; @@ -165,7 +165,7 @@ public async Task Partial_patching_resource_produces_expected_request() var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); var requestDocument = new AirplanePatchRequestDocument { @@ -217,7 +217,7 @@ public async Task Deleting_resource_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act await apiClient.DeleteFlightAsync(flightId); @@ -235,7 +235,7 @@ public async Task Getting_secondary_resource_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightOperatingAirplaneAsync(flightId)); @@ -254,7 +254,7 @@ public async Task Getting_secondary_resources_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightFlightAttendantsAsync(flightId)); @@ -273,7 +273,7 @@ public async Task Getting_ToOne_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId)); @@ -292,7 +292,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); var requestDocument = new ToOneAirplaneRequestData { @@ -328,7 +328,7 @@ public async Task Getting_ToMany_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightFlightAttendantsRelationshipAsync(flightId)); @@ -347,7 +347,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -397,7 +397,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -447,7 +447,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { diff --git a/test/OpenApiTests/ClientLibrary/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs similarity index 94% rename from test/OpenApiTests/ClientLibrary/ResponseTests.cs rename to test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 949566ab1a..aa6866cde2 100644 --- a/test/OpenApiTests/ClientLibrary/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -6,14 +6,14 @@ using System.Threading.Tasks; using FluentAssertions; using FluentAssertions.Specialized; -using OpenApiTests.ClientLibrary.GeneratedCode; +using OpenApiClientTests.LegacyClient.GeneratedCode; using TestBuildingBlocks; using Xunit; #pragma warning disable AV1500 // Member or local function contains too many statements #pragma warning disable AV1704 // Don't include numbers in variables, parameters and type members -namespace OpenApiTests.ClientLibrary +namespace OpenApiClientTests.LegacyClient { public sealed class ResponseTests { @@ -99,7 +99,7 @@ public async Task Getting_resource_collection_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act FlightCollectionResponseDocument document = await apiClient.GetFlightCollectionAsync(); @@ -174,7 +174,7 @@ public async Task Getting_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act FlightPrimaryResponseDocument document = await apiClient.GetFlightAsync(Convert.ToInt32(flightId)); @@ -209,7 +209,7 @@ public async Task Getting_unknown_resource_translates_error_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NotFound, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act Func> action = async () => await apiClient.GetFlightAsync(Convert.ToInt32(flightId)); @@ -271,7 +271,7 @@ public async Task Posting_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.Created, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act FlightPrimaryResponseDocument document = await apiClient.PostFlightAsync(new FlightPostRequestDocument @@ -322,7 +322,7 @@ public async Task Patching_resource_with_side_effects_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act FlightPrimaryResponseDocument document = await apiClient.PatchFlightAsync(Convert.ToInt32(flightId), new FlightPatchRequestDocument @@ -346,7 +346,7 @@ public async Task Patching_resource_without_side_effects_translates_response() // Arrange const string flightId = "8712"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await apiClient.PatchFlightAsync(Convert.ToInt32(flightId), @@ -368,7 +368,7 @@ public async Task Deleting_resource_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act Func action = async () => await apiClient.DeleteFlightAsync(8712); @@ -393,7 +393,7 @@ public async Task Getting_secondary_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act AirplaneSecondaryResponseDocument document = await apiClient.GetFlightOperatingAirplaneAsync(Convert.ToInt32(flightId)); @@ -417,7 +417,7 @@ public async Task Getting_secondary_resources_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act FlightAttendantCollectionResponseDocument document = await apiClient.GetFlightFlightAttendantsAsync(Convert.ToInt32(flightId)); @@ -445,7 +445,7 @@ public async Task Getting_ToOne_relationship_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act AirplaneIdentifierResponseDocument document = await apiClient.GetFlightOperatingAirplaneRelationshipAsync(Convert.ToInt32(flightId)); @@ -461,7 +461,7 @@ public async Task Patching_ToOne_relationship_translates_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act await apiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData @@ -499,7 +499,7 @@ public async Task Getting_ToMany_relationship_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act FlightAttendantIdentifierCollectionResponseDocument @@ -518,7 +518,7 @@ public async Task Posting_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act Func action = async () => await apiClient.PostFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData @@ -547,7 +547,7 @@ public async Task Patching_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act Func action = async () => await apiClient.PatchFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData @@ -576,7 +576,7 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); + ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act Func action = async () => await apiClient.DeleteFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj new file mode 100644 index 0000000000..bf3a11d666 --- /dev/null +++ b/test/OpenApiClientTests/OpenApiClientTests.csproj @@ -0,0 +1,49 @@ + + + $(NetCoreAppVersion) + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + OpenApiClientTests.LegacyClient.GeneratedCode + LegacyClient + NSwagCSharp + /UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal + + + + + + + + + + + + + + diff --git a/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs b/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs deleted file mode 100644 index 399e93b663..0000000000 --- a/test/OpenApiTests/ClientLibrary/GeneratedCode/IOpenApiClient.cs +++ /dev/null @@ -1,9 +0,0 @@ -using JsonApiDotNetCore.OpenApi.Client; - -namespace OpenApiTests.ClientLibrary.GeneratedCode -{ - // ReSharper disable once MemberCanBeInternal - partial interface IOpenApiClient : IJsonApiClient - { - } -} From f71bbeac8d4abc81cd29d77fd01981ab86c6aa03 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 16 Sep 2021 00:34:03 +0200 Subject: [PATCH 18/35] Some adjustments to stay closer to 1-on-1 model mapping from the private repository of the legacy integration. Also processed some of the review feedback --- .../ArgumentGuard.cs | 2 +- .../JsonApiClient.cs | 2 +- .../Properties/AssemblyInfo.cs | 1 - ...lientAttributeRegistrationLifeTimeTests.cs | 33 ++--- .../GeneratedCode/ILegacyClient.cs | 2 +- .../LegacyClient/RequestTests.cs | 20 ++- .../LegacyClient/ResponseTests.cs | 9 +- .../LegacyOpenApiIntegration/Airline.cs | 2 +- .../LegacyOpenApiIntegration/Airplane.cs | 26 ++-- .../LegacyOpenApiIntegration/Flight.cs | 6 +- .../FlightAttendant.cs | 4 +- .../FlightAttendantsController.cs | 4 +- .../LegacyOpenApiIntegrationStartup.cs | 1 - .../LegacyOpenApiIntegration/swagger.json | 128 ++++++++++++------ test/OpenApiTests/OpenApiTests.csproj | 22 --- 15 files changed, 141 insertions(+), 121 deletions(-) diff --git a/src/JsonApiDotNetCore.OpenApi.Client/ArgumentGuard.cs b/src/JsonApiDotNetCore.OpenApi.Client/ArgumentGuard.cs index 6feadb9a00..e0531017af 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/ArgumentGuard.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/ArgumentGuard.cs @@ -18,4 +18,4 @@ public static void NotNull([CanBeNull] [NoEnumeration] T value, [NotNull] [In } } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs index 55a9e73176..79be3be736 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiClient.cs @@ -220,4 +220,4 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ } } } -} \ No newline at end of file +} diff --git a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs index bb49f8748a..f4a8cf07c3 100644 --- a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs @@ -1,7 +1,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApi")] -[assembly: InternalsVisibleTo("JsonApiDotNetCore.OpenApiClient")] [assembly: InternalsVisibleTo("Benchmarks")] [assembly: InternalsVisibleTo("JsonApiDotNetCoreTests")] [assembly: InternalsVisibleTo("UnitTests")] diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs index c876a2497d..4c82fa119c 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs @@ -21,7 +21,6 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; - var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); var requestDocument = new AirplanePatchRequestDocument { @@ -29,10 +28,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r { Id = airplaneId, Type = AirplanesResourceType.Airplanes, - Attributes = new AirplaneAttributesInPatchRequest - { - ManufacturedAt = manufacturedAt - } + Attributes = new AirplaneAttributesInPatchRequest() } }; @@ -53,7 +49,6 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r ""type"": ""airplanes"", ""id"": """ + airplaneId + @""", ""attributes"": { - ""manufactured-at"": ""2021-01-01T15:23:05.033+04:00"", ""is-in-maintenance"": false } } @@ -135,10 +130,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ { Id = airplaneId2, Type = AirplanesResourceType.Airplanes, - Attributes = new AirplaneAttributesInPatchRequest - { - ManufacturedAt = manufacturedAt - } + Attributes = new AirplaneAttributesInPatchRequest() } }; @@ -146,7 +138,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ airplane => airplane.AirtimeInHours)) { using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, - airplane => airplane.ManufacturedAt)) + airplane => airplane.IsInMaintenance)) { } @@ -160,7 +152,6 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ ""type"": ""airplanes"", ""id"": """ + airplaneId2 + @""", ""attributes"": { - ""manufactured-at"": ""2021-01-01T15:23:05.033+04:00"", ""is-in-maintenance"": false } } @@ -296,7 +287,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att Type = AirplanesResourceType.Airplanes, Attributes = new AirplaneAttributesInPatchRequest { - SerialNumber = "100" + ManufacturedInCity = "Everett" } } }; @@ -304,7 +295,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, - airplane => airplane.LastServicedAt)) + airplane => airplane.SerialNumber)) { // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); @@ -316,8 +307,8 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att ""type"": ""airplanes"", ""id"": """ + airplaneId2 + @""", ""attributes"": { - ""last-serviced-at"": null, - ""serial-number"": ""100"" + ""serial-number"": null, + ""manufactured-in-city"": ""Everett"" } } }"); @@ -355,7 +346,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att Type = AirplanesResourceType.Airplanes, Attributes = new AirplaneAttributesInPatchRequest { - SerialNumber = "100" + ManufacturedInCity = "Everett" } } }; @@ -363,7 +354,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, - airplane => airplane.LastServicedAt)) + airplane => airplane.SerialNumber)) { // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument2)); @@ -375,8 +366,8 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att ""type"": ""airplanes"", ""id"": """ + airplaneId + @""", ""attributes"": { - ""last-serviced-at"": null, - ""serial-number"": ""100"" + ""serial-number"": null, + ""manufactured-in-city"": ""Everett"" } } }"); @@ -414,7 +405,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re }; using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, - airplane => airplane.ManufacturedAt)) + airplane => airplane.SerialNumber)) { using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) diff --git a/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs b/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs index c3d70d1da3..f207b8b19d 100644 --- a/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs +++ b/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs @@ -3,7 +3,7 @@ namespace OpenApiClientTests.LegacyClient.GeneratedCode { // ReSharper disable once MemberCanBeInternal - partial interface ILegacyClient : IJsonApiClient + public partial interface ILegacyClient : IJsonApiClient { } } diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 700138d8ba..f4434cd533 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -125,13 +125,13 @@ public async Task Partial_posting_resource_produces_expected_request() Attributes = new AirplaneAttributesInPostRequest { Name = name, - SeatingCapacity = 250 + AirtimeInHours = 800 } } }; using (apiClient.RegisterAttributesForRequestDocument(requestDocument, - airplane => airplane.AirtimeInHours)) + airplane => airplane.SerialNumber)) { // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument)); @@ -150,8 +150,8 @@ public async Task Partial_posting_resource_produces_expected_request() ""type"": ""airplanes"", ""attributes"": { ""name"": """ + name + @""", - ""airtime-in-hours"": null, - ""seating-capacity"": 250 + ""airtime-in-hours"": 800, + ""serial-number"": null } } }"); @@ -162,7 +162,7 @@ public async Task Partial_patching_resource_produces_expected_request() { // Arrange const string airplaneId = "XUuiP"; - var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); + var lastServicedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); @@ -175,14 +175,13 @@ public async Task Partial_patching_resource_produces_expected_request() Type = AirplanesResourceType.Airplanes, Attributes = new AirplaneAttributesInPatchRequest { - ManufacturedAt = manufacturedAt + LastServicedAt = lastServicedAt } } }; using (apiClient.RegisterAttributesForRequestDocument(requestDocument, - airplane => airplane.ManufacturedAt, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, - airplane => airplane.AirtimeInHours)) + airplane => airplane.SerialNumber, airplane => airplane.AirtimeInHours)) { // Act _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); @@ -201,10 +200,9 @@ public async Task Partial_patching_resource_produces_expected_request() ""type"": ""airplanes"", ""id"": ""XUuiP"", ""attributes"": { - ""manufactured-at"": ""2021-01-01T15:23:05.033+04:00"", ""airtime-in-hours"": null, - ""last-serviced-at"": null, - ""is-in-maintenance"": false + ""last-serviced-at"": ""2021-01-01T15:23:05.033+04:00"", + ""serial-number"": null } } }"); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index aa6866cde2..dfa7285719 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -49,7 +49,8 @@ public async Task Getting_resource_collection_translates_response() ""type"": ""flights"", ""id"": """ + flightId + @""", ""attributes"": { - ""destination"": """ + flightDestination + @""", + ""final-destination"": """ + flightDestination + @""", + ""stop-over-destination"": null, ""operated-by"": ""DeltaAirLines"", ""departs-at"": """ + flightDepartsAt + @""", ""arrives-at"": null, @@ -120,7 +121,7 @@ public async Task Getting_resource_collection_translates_response() flight.Meta.Should().HaveCount(1); flight.Meta["docs"].Should().Be(flightMetaValue); - flight.Attributes.Destination.Should().Be(flightDestination); + flight.Attributes.FinalDestination.Should().Be(flightDestination); flight.Attributes.ServicesOnBoard.Should().HaveCount(3); flight.Attributes.ServicesOnBoard.ElementAt(0).Should().Be(fightServiceOnBoard); flight.Attributes.ServicesOnBoard.ElementAt(1).Should().Be(string.Empty); @@ -187,7 +188,7 @@ public async Task Getting_resource_translates_response() 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.Destination.Should().BeNull(); + document.Data.Attributes.FinalDestination.Should().BeNull(); document.Data.Attributes.OperatedBy.Should().Be(default(Airline)); } @@ -203,7 +204,7 @@ public async Task Getting_unknown_resource_translates_error_response() ""id"": ""f1a520ac-02a0-466b-94ea-86cbaa86f02f"", ""status"": ""404"", ""destination"": ""The requested resource does not exist."", - ""detail"": ""Resource of type 'meetings' with ID '" + flightId + @"' does not exist."" + ""detail"": ""Resource of type 'flights' with ID '" + flightId + @"' does not exist."" } ] }"; diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Airline.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Airline.cs index 7c0725a149..959aec6fbc 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Airline.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Airline.cs @@ -3,7 +3,7 @@ namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public enum Airline + public enum Airline : byte { DeltaAirLines, LufthansaGroup, diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs index efb9efb977..cefb5e5fcd 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs @@ -15,29 +15,33 @@ public sealed class Airplane : Identifiable [MaxLength(255)] public string Name { get; set; } - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] - public DateTime ManufacturedAt { get; set; } - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] public int? AirtimeInHours { get; set; } - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] public DateTime? LastServicedAt { get; set; } [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] public bool IsInMaintenance { get; set; } - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] - [MaxLength(2000)] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] + [MaxLength(16)] public string SerialNumber { get; set; } - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] - public int SeatingCapacity { get; set; } - - [Attr(PublicName = "airplane-type", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] - public AircraftType AircraftType { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.All)] + [MaxLength(85)] + public string ManufacturedInCity { get; set; } [HasMany] public ISet Flights { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + public DateTime ManufacturedAt { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] + public long DistanceTraveledInKilometers { get; set; } + + [Attr(PublicName = "airplane-type", Capabilities = AttrCapabilities.AllowView)] + public AircraftType AircraftType { get; set; } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs index f948110a41..4e9af303d2 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs @@ -13,7 +13,11 @@ public sealed class Flight : Identifiable [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] [Required] [MaxLength(40)] - public string Destination { get; set; } + public string FinalDestination { get; set; } + + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + [MaxLength(2000)] + public string StopOverDestination { get; set; } [Attr] public DateTime? DepartsAt { get; set; } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs index 6e19636045..a66f20f6bb 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs @@ -7,10 +7,10 @@ namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class FlightAttendant : Identifiable + public sealed class FlightAttendant : Identifiable { [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowFilter)] - public override string Id { get; set; } + public override long Id { get; set; } [Attr(Capabilities = AttrCapabilities.None)] public FlightAttendantExpertiseLevel ExpertiseLevel { get; set; } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs index 43adc8d0eb..2eae98caba 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs @@ -5,9 +5,9 @@ namespace OpenApiTests.LegacyOpenApiIntegration { - public sealed class FlightAttendantsController : JsonApiController + public sealed class FlightAttendantsController : JsonApiController { - public FlightAttendantsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + public FlightAttendantsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs index 85dfd3d1e8..f460f1d7ef 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs @@ -20,7 +20,6 @@ protected override void SetJsonApiOptions(JsonApiOptions options) options.MaximumPageSize = new PageSize(100); options.MaximumPageNumber = new PageNumber(50); options.IncludeTotalResourceCount = true; - options.ValidateModelState = true; options.DefaultAttrCapabilities = AttrCapabilities.AllowView; options.SerializerSettings.ContractResolver = new DefaultContractResolver diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index 6995c9d1c7..ea706ea77e 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -478,7 +478,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -506,7 +507,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -534,7 +536,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -574,7 +577,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -597,7 +601,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -625,7 +630,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -655,7 +661,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -683,7 +690,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -711,7 +719,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -741,7 +750,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -771,7 +781,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -803,7 +814,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -831,7 +843,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -861,7 +874,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -889,7 +903,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -917,7 +932,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -947,7 +963,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -977,7 +994,8 @@ "in": "path", "required": true, "schema": { - "type": "string" + "type": "integer", + "format": "int64" } } ], @@ -1792,10 +1810,6 @@ "maxLength": 255, "type": "string" }, - "manufactured-at": { - "type": "string", - "format": "date-time" - }, "airtime-in-hours": { "type": "integer", "format": "int32", @@ -1810,7 +1824,12 @@ "type": "boolean" }, "serial-number": { - "maxLength": 2000, + "maxLength": 16, + "type": "string", + "nullable": true + }, + "manufactured-in-city": { + "maxLength": 85, "type": "string", "nullable": true } @@ -1827,21 +1846,33 @@ "maxLength": 255, "type": "string" }, - "manufactured-at": { - "type": "string", - "format": "date-time" - }, "airtime-in-hours": { "type": "integer", "format": "int32", "nullable": true }, - "seating-capacity": { - "type": "integer", - "format": "int32" + "last-serviced-at": { + "type": "string", + "format": "date-time", + "nullable": true }, - "airplane-type": { - "$ref": "#/components/schemas/aircraft-type" + "serial-number": { + "maxLength": 16, + "type": "string", + "nullable": true + }, + "manufactured-in-city": { + "maxLength": 85, + "type": "string", + "nullable": true + }, + "manufactured-at": { + "type": "string", + "format": "date-time" + }, + "distance-traveled-in-kilometers": { + "type": "integer", + "format": "int64" } }, "additionalProperties": false @@ -1853,10 +1884,6 @@ "maxLength": 255, "type": "string" }, - "manufactured-at": { - "type": "string", - "format": "date-time" - }, "airtime-in-hours": { "type": "integer", "format": "int32", @@ -1871,13 +1898,22 @@ "type": "boolean" }, "serial-number": { - "maxLength": 2000, + "maxLength": 16, "type": "string", "nullable": true }, - "seating-capacity": { + "manufactured-in-city": { + "maxLength": 85, + "type": "string", + "nullable": true + }, + "manufactured-at": { + "type": "string", + "format": "date-time" + }, + "distance-traveled-in-kilometers": { "type": "integer", - "format": "int32" + "format": "int64" }, "airplane-type": { "$ref": "#/components/schemas/aircraft-type" @@ -2441,10 +2477,15 @@ "flight-attributes-in-patch-request": { "type": "object", "properties": { - "destination": { + "final-destination": { "maxLength": 40, "type": "string" }, + "stop-over-destination": { + "maxLength": 2000, + "type": "string", + "nullable": true + }, "operated-by": { "$ref": "#/components/schemas/airline" } @@ -2454,10 +2495,15 @@ "flight-attributes-in-response": { "type": "object", "properties": { - "destination": { + "final-destination": { "maxLength": 40, "type": "string" }, + "stop-over-destination": { + "maxLength": 2000, + "type": "string", + "nullable": true + }, "departs-at": { "type": "string", "format": "date-time", diff --git a/test/OpenApiTests/OpenApiTests.csproj b/test/OpenApiTests/OpenApiTests.csproj index 964ff0ab7d..04355533a7 100644 --- a/test/OpenApiTests/OpenApiTests.csproj +++ b/test/OpenApiTests/OpenApiTests.csproj @@ -13,28 +13,6 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - OpenApiTests.ClientLibrary.GeneratedCode - OpenApiClient - NSwagCSharp - /UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal - From 348dffa6352afd04902def58c71696132f0ca7cb Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 16 Sep 2021 01:08:10 +0200 Subject: [PATCH 19/35] review feedback --- docs/usage/openapi/client-library.md | 2 +- .../JsonApiDotNetCoreExampleClient/Program.cs | 3 +-- .../JsonApiDotNetCore.OpenApi.Client.csproj | 2 +- .../JsonApiMetadata/EndpointResolver.cs | 14 ++++++++++++-- .../SwaggerComponents/JsonApiSchemaGenerator.cs | 6 +++--- .../ClientAttributeRegistrationLifeTimeTests.cs | 15 ++++++--------- .../LegacyClient}/FakeHttpClientWrapper.cs | 4 ++-- .../LegacyClient/GeneratedCode/LegacyClient.cs | 2 -- .../LegacyClient/RequestTests.cs | 2 -- .../LegacyClient/ResponseTests.cs | 2 -- .../LegacyOpenApiIntegration/Flight.cs | 2 ++ .../LegacyIntegrationDbContext.cs | 3 --- .../LegacyOpenApiIntegrationStartup.cs | 1 - .../OpenApiTests/{Startups => }/OpenApiStartup.cs | 2 +- 14 files changed, 29 insertions(+), 31 deletions(-) rename test/{TestBuildingBlocks => OpenApiClientTests/LegacyClient}/FakeHttpClientWrapper.cs (97%) rename test/OpenApiTests/{Startups => }/OpenApiStartup.cs (96%) diff --git a/docs/usage/openapi/client-library.md b/docs/usage/openapi/client-library.md index 129f9ac3ca..1cdca4807c 100644 --- a/docs/usage/openapi/client-library.md +++ b/docs/usage/openapi/client-library.md @@ -1,6 +1,6 @@ # OpenAPI Client Library -You can generate a client library in various programming languages from the OpenAPI specification file that JsonApiDotNetCore APIs provide. For C# .NET clients generated using NSwag, we provide an additional package that introduces support for sending partial write requests. +You can generate a client library in various programming languages from the OpenAPI specification file that JsonApiDotNetCore APIs provide. For C# .NET clients generated using NSwag, we provide an additional package that introduces better support for PATCH and POST requests. The issue here is that a property on a generated C# class being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". ## Installation diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs index bfdfdc469a..acf5182cc4 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs @@ -5,8 +5,7 @@ namespace JsonApiDotNetCoreExampleClient { - // ReSharper disable once ClassNeverInstantiated.Global - internal class Program + internal static class Program { private static async Task Main() { diff --git a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj index f7dbcd4b59..e5d66557fb 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj +++ b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj @@ -7,7 +7,7 @@ jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net;openapi;swagger;client;nswag - Provides support for partial PATCH and partial POST requests for a C# client generated with NSwag. + Provides support for OpenAPI generated clients in sending partial POST/PATCH requests against JSON:API endpoints json-api-dotnet https://www.jsonapi.net/ MIT diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs index 810c96ab69..fae0d1fbc6 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/EndpointResolver.cs @@ -12,8 +12,8 @@ internal sealed class EndpointResolver { ArgumentGuard.NotNull(controllerAction, nameof(controllerAction)); - if (!typeof(CoreJsonApiController).IsAssignableFrom(controllerAction.ReflectedType) || - typeof(BaseJsonApiOperationsController).IsAssignableFrom(controllerAction.ReflectedType)) + // This is a temporary work-around to prevent the JsonApiDotNetCoreExample project from crashing upon startup. + if (!IsJsonApiController(controllerAction) || IsOperationsController(controllerAction)) { return null; } @@ -23,6 +23,16 @@ internal sealed class EndpointResolver return ResolveJsonApiEndpoint(method); } + private static bool IsJsonApiController(MethodInfo controllerAction) + { + return typeof(CoreJsonApiController).IsAssignableFrom(controllerAction.ReflectedType); + } + + private static bool IsOperationsController(MethodInfo controllerAction) + { + return typeof(BaseJsonApiOperationsController).IsAssignableFrom(controllerAction.ReflectedType); + } + private static JsonApiEndpoint? ResolveJsonApiEndpoint(HttpMethodAttribute httpMethod) { return httpMethod switch diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index 3b506a86b9..b1589e2004 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -107,12 +107,12 @@ private OpenApiSchema GenerateResourceJsonApiDocumentSchema(Type type) referenceSchemaForResourceObject = _resourceObjectSchemaGenerator.GenerateSchema(resourceObjectType); } - OpenApiSchema referenceSchemaForDataObject = - IsSingleDataDocument(type) ? referenceSchemaForResourceObject : CreateArrayTypeDataSchema(referenceSchemaForResourceObject); - OpenApiSchema referenceSchemaForDocument = _defaultSchemaGenerator.GenerateSchema(type, _schemaRepositoryAccessor.Current); OpenApiSchema fullSchemaForDocument = _schemaRepositoryAccessor.Current.Schemas[referenceSchemaForDocument.Reference.Id]; + OpenApiSchema referenceSchemaForDataObject = + IsSingleDataDocument(type) ? referenceSchemaForResourceObject : CreateArrayTypeDataSchema(referenceSchemaForResourceObject); + fullSchemaForDocument.Properties[JsonApiObjectPropertyName.Data] = referenceSchemaForDataObject; return referenceSchemaForDocument; diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs index 4c82fa119c..7cd175baa7 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs @@ -1,8 +1,6 @@ using System.Net; using System.Threading.Tasks; using FluentAssertions; -using FluentAssertions.Common; -using FluentAssertions.Extensions; using OpenApiClientTests.LegacyClient.GeneratedCode; using TestBuildingBlocks; using Xunit; @@ -58,10 +56,10 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r [Fact] public async Task Attribute_registration_can_be_used_for_multiple_requests() { + // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); - // Arrange const string airplaneId = "XUuiP"; var requestDocument = new AirplanePatchRequestDocument @@ -110,7 +108,6 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; - var manufacturedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); var requestDocument1 = new AirplanePatchRequestDocument { @@ -161,10 +158,10 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ [Fact] public async Task Attribute_values_can_be_changed_after_attribute_registration() { + // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); - // Arrange const string airplaneId = "XUuiP"; var requestDocument = new AirplanePatchRequestDocument @@ -204,10 +201,10 @@ 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() { + // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); - // Arrange const string airplaneId1 = "XUuiP"; var requestDocument1 = new AirplanePatchRequestDocument @@ -255,10 +252,10 @@ 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() { + // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); - // Arrange const string airplaneId1 = "XUuiP"; var requestDocument1 = new AirplanePatchRequestDocument @@ -317,10 +314,10 @@ 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() { + // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); - // Arrange var requestDocument1 = new AirplanePostRequestDocument { Data = new AirplaneDataInPostRequest @@ -376,10 +373,10 @@ 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() { + // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); - // Arrange const string airplaneId1 = "XUuiP"; var requestDocument1 = new AirplanePatchRequestDocument diff --git a/test/TestBuildingBlocks/FakeHttpClientWrapper.cs b/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs similarity index 97% rename from test/TestBuildingBlocks/FakeHttpClientWrapper.cs rename to test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs index 537cea9d19..f1a4abcfa7 100644 --- a/test/TestBuildingBlocks/FakeHttpClientWrapper.cs +++ b/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs @@ -6,9 +6,9 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using JsonApiDotNetCore; +using JsonApiDotNetCore.OpenApi.Client; -namespace TestBuildingBlocks +namespace OpenApiClientTests.LegacyClient { /// diff --git a/test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs b/test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs index 7d023412e3..e6ba211021 100644 --- a/test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs +++ b/test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs @@ -9,9 +9,7 @@ partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) { SetSerializerSettingsForJsonApi(settings); -#if DEBUG settings.Formatting = Formatting.Indented; -#endif } } } diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index f4434cd533..782f8e10c9 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -11,8 +11,6 @@ using TestBuildingBlocks; using Xunit; -#pragma warning disable AV1500 // Member or local function contains too many statements - namespace OpenApiClientTests.LegacyClient { public sealed class RequestTests diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index dfa7285719..bf3e9cdca5 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -7,10 +7,8 @@ using FluentAssertions; using FluentAssertions.Specialized; using OpenApiClientTests.LegacyClient.GeneratedCode; -using TestBuildingBlocks; using Xunit; -#pragma warning disable AV1500 // Member or local function contains too many statements #pragma warning disable AV1704 // Don't include numbers in variables, parameters and type members namespace OpenApiClientTests.LegacyClient diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs index 4e9af303d2..34dc7c5ebd 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; using JetBrains.Annotations; using JsonApiDotNetCore.Resources; using JsonApiDotNetCore.Resources.Annotations; @@ -29,6 +30,7 @@ public sealed class Flight : Identifiable public Airline Airline { get; set; } [Attr] + [NotMapped] public ICollection ServicesOnBoard { get; set; } [HasOne] diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs index ff022c1879..a9ca8524ed 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs @@ -27,9 +27,6 @@ protected override void OnModelCreating(ModelBuilder builder) builder.Entity() .HasMany(flight => flight.BackupPersonnel) .WithMany(flightAttendant => flightAttendant.StandbyForFlights); - - builder.Entity() - .Ignore(flight => flight.ServicesOnBoard); } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs index f460f1d7ef..bea8048c14 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs @@ -3,7 +3,6 @@ using JsonApiDotNetCore.Resources.Annotations; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json.Serialization; -using OpenApiTests.Startups; namespace OpenApiTests.LegacyOpenApiIntegration { diff --git a/test/OpenApiTests/Startups/OpenApiStartup.cs b/test/OpenApiTests/OpenApiStartup.cs similarity index 96% rename from test/OpenApiTests/Startups/OpenApiStartup.cs rename to test/OpenApiTests/OpenApiStartup.cs index df477372cd..992ea57218 100644 --- a/test/OpenApiTests/Startups/OpenApiStartup.cs +++ b/test/OpenApiTests/OpenApiStartup.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; using TestBuildingBlocks; -namespace OpenApiTests.Startups +namespace OpenApiTests { public abstract class OpenApiStartup : TestableStartup where TDbContext : DbContext From e88c7621e0994a36a099deb24b23180369cab379 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 16 Sep 2021 01:40:03 +0200 Subject: [PATCH 20/35] Process inspectcode issues that do not appear when run locally --- test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs b/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs index f1a4abcfa7..33fbe4ed05 100644 --- a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs +++ b/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs @@ -14,7 +14,7 @@ namespace OpenApiClientTests.LegacyClient /// /// Enables to inject an outgoing response body and inspect the incoming request. /// - public sealed class FakeHttpClientWrapper : IDisposable + internal sealed class FakeHttpClientWrapper : IDisposable { private readonly FakeHttpMessageHandler _handler; From 54ac898c6460eba8bfa20dc42c88c222495e2151 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 16 Sep 2021 08:01:45 +0200 Subject: [PATCH 21/35] Update package reference in build script --- Build.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Build.ps1 b/Build.ps1 index 29cbf44f17..0b5b8d190d 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -86,12 +86,12 @@ function CreateNuGetPackage { if ([string]::IsNullOrWhitespace($versionSuffix)) { dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts dotnet pack .\src\JsonApiDotNetCore.OpenApi -c Release -o .\artifacts - dotnet pack .\src\JsonApiDotNetCore.OpenApiClient -c Release -o .\artifacts + dotnet pack .\src\JsonApiDotNetCore.OpenApi.Client -c Release -o .\artifacts } else { dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts --version-suffix=$versionSuffix dotnet pack .\src\JsonApiDotNetCore.OpenApi -c Release -o .\artifacts --version-suffix=$versionSuffix - dotnet pack .\src\JsonApiDotNetCore.OpenApiClient -c Release -o .\artifacts --version-suffix=$versionSuffix + dotnet pack .\src\JsonApiDotNetCore.OpenApi.Client -c Release -o .\artifacts --version-suffix=$versionSuffix } CheckLastExitCode From 83ac507eb433231050f443f905646971a98505f4 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 16 Sep 2021 16:13:57 +0200 Subject: [PATCH 22/35] Changed models to stay closer 1-to-1 mapping. Changed 'OpenApiClient' to 'OpenApi.Client'. Removed unneeded config from legacy integration startup. --- docs/usage/openapi/client-library.md | 10 +- .../Properties/AssemblyInfo.cs | 1 - .../LegacyClient/RequestTests.cs | 28 +- .../LegacyClient/ResponseTests.cs | 89 +++--- .../{AircraftType.cs => AircraftKind.cs} | 2 +- .../LegacyOpenApiIntegration/Airplane.cs | 25 +- .../LegacyOpenApiIntegration/Flight.cs | 20 +- .../FlightAttendant.cs | 5 +- .../LegacyIntegrationDbContext.cs | 2 +- .../LegacyOpenApiIntegrationStartup.cs | 4 - .../LegacyOpenApiIntegration/swagger.json | 255 ++++++++---------- 11 files changed, 209 insertions(+), 232 deletions(-) rename test/OpenApiTests/LegacyOpenApiIntegration/{AircraftType.cs => AircraftKind.cs} (88%) diff --git a/docs/usage/openapi/client-library.md b/docs/usage/openapi/client-library.md index 1cdca4807c..520d13e1e9 100644 --- a/docs/usage/openapi/client-library.md +++ b/docs/usage/openapi/client-library.md @@ -6,23 +6,23 @@ You can generate a client library in various programming languages from the Open You are required to install the following NuGet packages: -- `JsonApiDotNetCore.OpenApiClient` +- `JsonApiDotNetCore.OpenApi.Client` - `NSwag.ApiDescription.Client` - `Microsoft.Extensions.ApiDescription.Cient` - `NSwag.ApiDescription.Client` -The following examples demonstrate how to install the `JsonApiDotNetCore.OpenApiClient` package. +The following examples demonstrate how to install the `JsonApiDotNetCore.OpenApi.Client` package. ### CLI ``` -dotnet add package JsonApiDotNetCore.OpenApiClient +dotnet add package JsonApiDotNetCore.OpenApi.Client ``` ### Visual Studio ```powershell -Install-Package JsonApiDotNetCore.OpenApiClient +Install-Package JsonApiDotNetCore.OpenApi.Client ``` ### *.csproj @@ -30,7 +30,7 @@ Install-Package JsonApiDotNetCore.OpenApiClient ```xml - + ``` diff --git a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs index f4a8cf07c3..2cc2ee88bc 100644 --- a/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs +++ b/src/JsonApiDotNetCore/Properties/AssemblyInfo.cs @@ -5,5 +5,4 @@ [assembly: InternalsVisibleTo("JsonApiDotNetCoreTests")] [assembly: InternalsVisibleTo("UnitTests")] [assembly: InternalsVisibleTo("DiscoveryTests")] -[assembly: InternalsVisibleTo("OpenApiTests")] [assembly: InternalsVisibleTo("TestBuildingBlocks")] diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 782f8e10c9..a40f8e26f2 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -148,8 +148,8 @@ public async Task Partial_posting_resource_produces_expected_request() ""type"": ""airplanes"", ""attributes"": { ""name"": """ + name + @""", - ""airtime-in-hours"": 800, - ""serial-number"": null + ""serial-number"": null, + ""airtime-in-hours"": 800 } } }"); @@ -198,9 +198,9 @@ public async Task Partial_patching_resource_produces_expected_request() ""type"": ""airplanes"", ""id"": ""XUuiP"", ""attributes"": { + ""serial-number"": null, ""airtime-in-hours"": null, - ""last-serviced-at"": ""2021-01-01T15:23:05.033+04:00"", - ""serial-number"": null + ""last-serviced-at"": ""2021-01-01T15:23:05.033+04:00"" } } }"); @@ -253,12 +253,12 @@ public async Task Getting_secondary_resources_produces_expected_request() ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightFlightAttendantsAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCabinPersonnelAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/flight-attendants"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/cabin-personnel"); wrapper.RequestBody.Should().BeNull(); } @@ -327,12 +327,12 @@ public async Task Getting_ToMany_relationship_produces_expected_request() ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightFlightAttendantsRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCabinPersonnelRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/flight-attendants"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-personnel"); wrapper.RequestBody.Should().BeNull(); } @@ -363,11 +363,11 @@ public async Task Posting_ToMany_relationship_produces_expected_request() }; // Act - await apiClient.PostFlightFlightAttendantsRelationshipAsync(flightId, requestDocument); + await apiClient.PostFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Post); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/flight-attendants"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-personnel"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -413,11 +413,11 @@ public async Task Patching_ToMany_relationship_produces_expected_request() }; // Act - await apiClient.PatchFlightFlightAttendantsRelationshipAsync(flightId, requestDocument); + await apiClient.PatchFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/flight-attendants"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-personnel"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -463,11 +463,11 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() }; // Act - await apiClient.DeleteFlightFlightAttendantsRelationshipAsync(flightId, requestDocument); + await apiClient.DeleteFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Delete); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/flight-attendants"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-personnel"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index bf3e9cdca5..5d48507dd6 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -28,8 +28,8 @@ public async Task Getting_resource_collection_translates_response() const string documentMetaValue = "1"; const string flightMetaValue = "https://api.jsonapi.net/docs/#get-flights"; const string operatingAirplaneMetaValue = "https://jsonapi.net/api/docs/#get-flight-operating-airplane"; - const string flightAttendantsMetaValue = "https://jsonapi.net/api/docs/#get-flight-flight-attendants"; - const string reserveFlightAttendantsMetaValue = "https://jsonapi.net/api/docs/#get-flight-reserve-flight-attendants"; + const string cabinPersonnelMetaValue = "https://jsonapi.net/api/docs/#get-flight-cabin-personnel"; + const string reserveCabinPersonnelMetaValue = "https://jsonapi.net/api/docs/#get-flight-backup-cabin-personnel"; const string topLevelLink = HostPrefix + "flights"; const string flightResourceLink = topLevelLink + "/" + flightId; @@ -68,22 +68,22 @@ public async Task Getting_resource_collection_translates_response() ""docs"": """ + operatingAirplaneMetaValue + @""" } }, - ""flight-attendants"": { + ""cabin-personnel"": { ""links"": { - ""self"": """ + flightResourceLink + @"/relationships/flight-attendants"", - ""related"": """ + flightResourceLink + @"/flight-attendants"" + ""self"": """ + flightResourceLink + @"/relationships/cabin-personnel"", + ""related"": """ + flightResourceLink + @"/cabin-personnel"" }, ""meta"": { - ""docs"": """ + flightAttendantsMetaValue + @""" + ""docs"": """ + cabinPersonnelMetaValue + @""" } }, - ""reserve-flight-attendants"": { + ""backup-cabin-personnel"": { ""links"": { - ""self"": """ + flightResourceLink + @"/relationships/reserve-flight-attendants"", - ""related"": """ + flightResourceLink + @"/reserve-flight-attendants"" + ""self"": """ + flightResourceLink + @"/relationships/backup-cabin-personnel"", + ""related"": """ + flightResourceLink + @"/backup-cabin-personnel"" }, ""meta"": { - ""docs"": """ + reserveFlightAttendantsMetaValue + @""" + ""docs"": """ + reserveCabinPersonnelMetaValue + @""" } } }, @@ -134,17 +134,17 @@ public async Task Getting_resource_collection_translates_response() flight.Relationships.OperatingAirplane.Meta.Should().HaveCount(1); flight.Relationships.OperatingAirplane.Meta["docs"].Should().Be(operatingAirplaneMetaValue); - flight.Relationships.FlightAttendants.Data.Should().BeNull(); - flight.Relationships.FlightAttendants.Links.Self.Should().Be(flightResourceLink + "/relationships/flight-attendants"); - flight.Relationships.FlightAttendants.Links.Related.Should().Be(flightResourceLink + "/flight-attendants"); - flight.Relationships.FlightAttendants.Meta.Should().HaveCount(1); - flight.Relationships.FlightAttendants.Meta["docs"].Should().Be(flightAttendantsMetaValue); - - flight.Relationships.ReserveFlightAttendants.Data.Should().BeNull(); - flight.Relationships.ReserveFlightAttendants.Links.Self.Should().Be(flightResourceLink + "/relationships/reserve-flight-attendants"); - flight.Relationships.ReserveFlightAttendants.Links.Related.Should().Be(flightResourceLink + "/reserve-flight-attendants"); - flight.Relationships.ReserveFlightAttendants.Meta.Should().HaveCount(1); - flight.Relationships.ReserveFlightAttendants.Meta["docs"].Should().Be(reserveFlightAttendantsMetaValue); + flight.Relationships.CabinPersonnel.Data.Should().BeNull(); + flight.Relationships.CabinPersonnel.Links.Self.Should().Be(flightResourceLink + "/relationships/cabin-personnel"); + flight.Relationships.CabinPersonnel.Links.Related.Should().Be(flightResourceLink + "/cabin-personnel"); + flight.Relationships.CabinPersonnel.Meta.Should().HaveCount(1); + flight.Relationships.CabinPersonnel.Meta["docs"].Should().Be(cabinPersonnelMetaValue); + + flight.Relationships.BackupCabinPersonnel.Data.Should().BeNull(); + flight.Relationships.BackupCabinPersonnel.Links.Self.Should().Be(flightResourceLink + "/relationships/backup-cabin-personnel"); + flight.Relationships.BackupCabinPersonnel.Links.Related.Should().Be(flightResourceLink + "/backup-cabin-personnel"); + flight.Relationships.BackupCabinPersonnel.Meta.Should().HaveCount(1); + flight.Relationships.BackupCabinPersonnel.Meta["docs"].Should().Be(reserveCabinPersonnelMetaValue); } [Fact] @@ -230,7 +230,7 @@ public async Task Posting_resource_translates_response() const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,flight-attendants,reserve-flight-attendants"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-personnel,backup-cabin-personnel"" }, ""data"": { ""type"": ""flights"", @@ -243,10 +243,10 @@ public async Task Posting_resource_translates_response() }, ""data"": null }, - ""flight-attendants"": { + ""cabin-personnel"": { ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/flight-attendants"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/flight-attendants"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-personnel"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-personnel"" }, ""data"": [ { @@ -255,16 +255,16 @@ public async Task Posting_resource_translates_response() } ], }, - ""reserve-flight-attendants"": { + ""backup-cabin-personnel"": { ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/reserve-flight-attendants"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/reserve-flight-attendants"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/backup-cabin-personnel"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/backup-cabin-personnel"" }, ""data"": [ ] } }, ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,flight-attendants,reserve-flight-attendants"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-personnel,backup-cabin-personnel"" } } }"; @@ -295,10 +295,10 @@ public async Task Posting_resource_translates_response() // Assert document.Data.Attributes.Should().BeNull(); document.Data.Relationships.OperatingAirplane.Data.Should().BeNull(); - document.Data.Relationships.FlightAttendants.Data.Should().HaveCount(1); - document.Data.Relationships.FlightAttendants.Data.First().Id.Should().Be(flightAttendantId); - document.Data.Relationships.FlightAttendants.Data.First().Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); - document.Data.Relationships.ReserveFlightAttendants.Data.Should().BeEmpty(); + document.Data.Relationships.CabinPersonnel.Data.Should().HaveCount(1); + document.Data.Relationships.CabinPersonnel.Data.First().Id.Should().Be(flightAttendantId); + document.Data.Relationships.CabinPersonnel.Data.First().Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); + document.Data.Relationships.BackupCabinPersonnel.Data.Should().BeEmpty(); } [Fact] @@ -315,7 +315,7 @@ public async Task Patching_resource_with_side_effects_translates_response() ""type"": ""flights"", ""id"": """ + flightId + @""", ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,flight-attendants,reserve-flight-attendants"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-personnel,backup-cabin-personnel"" } } }"; @@ -409,8 +409,8 @@ public async Task Getting_secondary_resources_translates_response() const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/flight-attendants"", - ""first"": """ + HostPrefix + @"flights/" + flightId + @"/flight-attendants"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-personnel"", + ""first"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-personnel"" }, ""data"": [ ] }"; @@ -419,7 +419,7 @@ public async Task Getting_secondary_resources_translates_response() ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act - FlightAttendantCollectionResponseDocument document = await apiClient.GetFlightFlightAttendantsAsync(Convert.ToInt32(flightId)); + FlightAttendantCollectionResponseDocument document = await apiClient.GetFlightCabinPersonnelAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().BeEmpty(); @@ -483,9 +483,9 @@ public async Task Getting_ToMany_relationship_translates_response() const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/flight-attendants"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/flight-attendants"", - ""first"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/flight-attendants"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-personnel"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-personnel"", + ""first"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-personnel"" }, ""data"": [{ ""type"": ""flight-attendants"", @@ -501,8 +501,7 @@ public async Task Getting_ToMany_relationship_translates_response() ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act - FlightAttendantIdentifierCollectionResponseDocument - document = await apiClient.GetFlightFlightAttendantsRelationshipAsync(Convert.ToInt32(flightId)); + FlightAttendantIdentifierCollectionResponseDocument document = await apiClient.GetFlightCabinPersonnelRelationshipAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().HaveCount(2); @@ -520,7 +519,7 @@ public async Task Posting_ToMany_relationship_produces_empty_response() ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.PostFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await apiClient.PostFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { @@ -549,7 +548,7 @@ public async Task Patching_ToMany_relationship_produces_empty_response() ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.PatchFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await apiClient.PatchFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { @@ -578,7 +577,7 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.DeleteFlightFlightAttendantsRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await apiClient.DeleteFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/AircraftType.cs b/test/OpenApiTests/LegacyOpenApiIntegration/AircraftKind.cs similarity index 88% rename from test/OpenApiTests/LegacyOpenApiIntegration/AircraftType.cs rename to test/OpenApiTests/LegacyOpenApiIntegration/AircraftKind.cs index 617a8a2a93..463499110f 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/AircraftType.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/AircraftKind.cs @@ -3,7 +3,7 @@ namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public enum AircraftType + public enum AircraftKind { Turboprops, LightJet, diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs index cefb5e5fcd..b086181524 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Airplane.cs @@ -15,33 +15,30 @@ public sealed class Airplane : Identifiable [MaxLength(255)] public string Name { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] + [MaxLength(16)] + public string SerialNumber { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] public int? AirtimeInHours { get; set; } [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] public DateTime? LastServicedAt { get; set; } + [Attr] + public DateTime ManufacturedAt { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] public bool IsInMaintenance { get; set; } - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] - [MaxLength(16)] - public string SerialNumber { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.All)] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] [MaxLength(85)] public string ManufacturedInCity { get; set; } + [Attr(Capabilities = AttrCapabilities.AllowView)] + public AircraftKind Kind { get; set; } + [HasMany] public ISet Flights { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] - public DateTime ManufacturedAt { get; set; } - - [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] - public long DistanceTraveledInKilometers { get; set; } - - [Attr(PublicName = "airplane-type", Capabilities = AttrCapabilities.AllowView)] - public AircraftType AircraftType { get; set; } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs index 34dc7c5ebd..1082c991a0 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs @@ -20,26 +20,26 @@ public sealed class Flight : Identifiable [MaxLength(2000)] public string StopOverDestination { get; set; } + [Attr(PublicName = "operated-by", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] + public Airline Airline { get; set; } + [Attr] public DateTime? DepartsAt { get; set; } [Attr] public DateTime? ArrivesAt { get; set; } - [Attr(PublicName = "operated-by", Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] - public Airline Airline { get; set; } - - [Attr] - [NotMapped] - public ICollection ServicesOnBoard { get; set; } + [HasMany] + public ISet CabinPersonnel { get; set; } [HasOne] public Airplane OperatingAirplane { get; set; } - [HasMany(PublicName = "flight-attendants")] - public ISet CabinPersonnel { get; set; } + [Attr] + [NotMapped] + public ICollection ServicesOnBoard { get; set; } - [HasMany(PublicName = "reserve-flight-attendants")] - public ICollection BackupPersonnel { get; set; } + [HasMany] + public ICollection BackupCabinPersonnel { get; set; } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs index a66f20f6bb..a76d3153c5 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs @@ -30,11 +30,14 @@ public sealed class FlightAttendant : Identifiable [Range(18, 75)] public int Age { get; set; } - [Attr(Capabilities = AttrCapabilities.All)] + [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowCreate)] [Required] [Url] public string ProfileImageUrl { get; set; } + [Attr] + public long DistanceTraveledInKilometers { get; set; } + [HasMany] public ISet ScheduledForFlights { get; set; } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs index a9ca8524ed..1672698234 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs @@ -25,7 +25,7 @@ protected override void OnModelCreating(ModelBuilder builder) .WithMany(flightAttendant => flightAttendant.ScheduledForFlights); builder.Entity() - .HasMany(flight => flight.BackupPersonnel) + .HasMany(flight => flight.BackupCabinPersonnel) .WithMany(flightAttendant => flightAttendant.StandbyForFlights); } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs index bea8048c14..02dbf96e25 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyOpenApiIntegrationStartup.cs @@ -15,10 +15,6 @@ protected override void SetJsonApiOptions(JsonApiOptions options) base.SetJsonApiOptions(options); options.Namespace = "api/v1"; - options.DefaultPageSize = new PageSize(10); - options.MaximumPageSize = new PageSize(100); - options.MaximumPageNumber = new PageNumber(50); - options.IncludeTotalResourceCount = true; options.DefaultAttrCapabilities = AttrCapabilities.AllowView; options.SerializerSettings.ContractResolver = new DefaultContractResolver diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index ea706ea77e..0fc85df9de 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -1206,12 +1206,12 @@ } } }, - "/api/v1/flights/{id}/flight-attendants": { + "/api/v1/flights/{id}/backup-cabin-personnel": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-flight-attendants", + "operationId": "get-flight-backup-cabin-personnel", "parameters": [ { "name": "id", @@ -1240,7 +1240,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-flight-attendants", + "operationId": "head-flight-backup-cabin-personnel", "parameters": [ { "name": "id", @@ -1266,12 +1266,12 @@ } } }, - "/api/v1/flights/{id}/relationships/flight-attendants": { + "/api/v1/flights/{id}/relationships/backup-cabin-personnel": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-flight-attendants-relationship", + "operationId": "get-flight-backup-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1300,7 +1300,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-flight-attendants-relationship", + "operationId": "head-flight-backup-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1329,7 +1329,7 @@ "tags": [ "flights" ], - "operationId": "post-flight-flight-attendants-relationship", + "operationId": "post-flight-backup-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1360,7 +1360,7 @@ "tags": [ "flights" ], - "operationId": "patch-flight-flight-attendants-relationship", + "operationId": "patch-flight-backup-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1391,7 +1391,7 @@ "tags": [ "flights" ], - "operationId": "delete-flight-flight-attendants-relationship", + "operationId": "delete-flight-backup-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1419,12 +1419,12 @@ } } }, - "/api/v1/flights/{id}/operating-airplane": { + "/api/v1/flights/{id}/cabin-personnel": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-operating-airplane", + "operationId": "get-flight-cabin-personnel", "parameters": [ { "name": "id", @@ -1442,7 +1442,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-secondary-response-document" + "$ref": "#/components/schemas/flight-attendant-collection-response-document" } } } @@ -1453,7 +1453,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-operating-airplane", + "operationId": "head-flight-cabin-personnel", "parameters": [ { "name": "id", @@ -1471,7 +1471,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-secondary-response-document" + "$ref": "#/components/schemas/flight-attendant-collection-response-document" } } } @@ -1479,12 +1479,12 @@ } } }, - "/api/v1/flights/{id}/relationships/operating-airplane": { + "/api/v1/flights/{id}/relationships/cabin-personnel": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-operating-airplane-relationship", + "operationId": "get-flight-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1502,7 +1502,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-identifier-response-document" + "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" } } } @@ -1513,7 +1513,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-operating-airplane-relationship", + "operationId": "head-flight-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1531,18 +1531,18 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-identifier-response-document" + "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" } } } } } }, - "patch": { + "post": { "tags": [ "flights" ], - "operationId": "patch-flight-operating-airplane-relationship", + "operationId": "post-flight-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1558,7 +1558,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-airplane-request-data" + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" } } } @@ -1568,14 +1568,12 @@ "description": "Success" } } - } - }, - "/api/v1/flights/{id}/reserve-flight-attendants": { - "get": { + }, + "patch": { "tags": [ "flights" ], - "operationId": "get-flight-reserve-flight-attendants", + "operationId": "patch-flight-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1587,24 +1585,26 @@ } } ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/flight-attendant-collection-response-document" - } + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" } } } + }, + "responses": { + "204": { + "description": "Success" + } } }, - "head": { + "delete": { "tags": [ "flights" ], - "operationId": "head-flight-reserve-flight-attendants", + "operationId": "delete-flight-cabin-personnel-relationship", "parameters": [ { "name": "id", @@ -1616,26 +1616,28 @@ } } ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/flight-attendant-collection-response-document" - } + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" } } } + }, + "responses": { + "204": { + "description": "Success" + } } } }, - "/api/v1/flights/{id}/relationships/reserve-flight-attendants": { + "/api/v1/flights/{id}/operating-airplane": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-reserve-flight-attendants-relationship", + "operationId": "get-flight-operating-airplane", "parameters": [ { "name": "id", @@ -1653,7 +1655,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" + "$ref": "#/components/schemas/airplane-secondary-response-document" } } } @@ -1664,7 +1666,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-reserve-flight-attendants-relationship", + "operationId": "head-flight-operating-airplane", "parameters": [ { "name": "id", @@ -1682,18 +1684,20 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" + "$ref": "#/components/schemas/airplane-secondary-response-document" } } } } } - }, - "post": { + } + }, + "/api/v1/flights/{id}/relationships/operating-airplane": { + "get": { "tags": [ "flights" ], - "operationId": "post-flight-reserve-flight-attendants-relationship", + "operationId": "get-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1705,26 +1709,24 @@ } } ], - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/airplane-identifier-response-document" + } } } } - }, - "responses": { - "204": { - "description": "Success" - } } }, - "patch": { + "head": { "tags": [ "flights" ], - "operationId": "patch-flight-reserve-flight-attendants-relationship", + "operationId": "head-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1736,26 +1738,24 @@ } } ], - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + "responses": { + "200": { + "description": "Success", + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/airplane-identifier-response-document" + } } } } - }, - "responses": { - "204": { - "description": "Success" - } } }, - "delete": { + "patch": { "tags": [ "flights" ], - "operationId": "delete-flight-reserve-flight-attendants-relationship", + "operationId": "patch-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1771,7 +1771,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + "$ref": "#/components/schemas/to-one-airplane-request-data" } } } @@ -1786,7 +1786,7 @@ }, "components": { "schemas": { - "aircraft-type": { + "aircraft-kind": { "enum": [ "Turboprops", "LightJet", @@ -1810,6 +1810,11 @@ "maxLength": 255, "type": "string" }, + "serial-number": { + "maxLength": 16, + "type": "string", + "nullable": true + }, "airtime-in-hours": { "type": "integer", "format": "int32", @@ -1823,11 +1828,6 @@ "is-in-maintenance": { "type": "boolean" }, - "serial-number": { - "maxLength": 16, - "type": "string", - "nullable": true - }, "manufactured-in-city": { "maxLength": 85, "type": "string", @@ -1846,6 +1846,11 @@ "maxLength": 255, "type": "string" }, + "serial-number": { + "maxLength": 16, + "type": "string", + "nullable": true + }, "airtime-in-hours": { "type": "integer", "format": "int32", @@ -1855,24 +1860,6 @@ "type": "string", "format": "date-time", "nullable": true - }, - "serial-number": { - "maxLength": 16, - "type": "string", - "nullable": true - }, - "manufactured-in-city": { - "maxLength": 85, - "type": "string", - "nullable": true - }, - "manufactured-at": { - "type": "string", - "format": "date-time" - }, - "distance-traveled-in-kilometers": { - "type": "integer", - "format": "int64" } }, "additionalProperties": false @@ -1884,6 +1871,11 @@ "maxLength": 255, "type": "string" }, + "serial-number": { + "maxLength": 16, + "type": "string", + "nullable": true + }, "airtime-in-hours": { "type": "integer", "format": "int32", @@ -1894,29 +1886,20 @@ "format": "date-time", "nullable": true }, + "manufactured-at": { + "type": "string", + "format": "date-time" + }, "is-in-maintenance": { "type": "boolean" }, - "serial-number": { - "maxLength": 16, - "type": "string", - "nullable": true - }, "manufactured-in-city": { "maxLength": 85, "type": "string", "nullable": true }, - "manufactured-at": { - "type": "string", - "format": "date-time" - }, - "distance-traveled-in-kilometers": { - "type": "integer", - "format": "int64" - }, - "airplane-type": { - "$ref": "#/components/schemas/aircraft-type" + "kind": { + "$ref": "#/components/schemas/aircraft-kind" } }, "additionalProperties": false @@ -2189,10 +2172,6 @@ "minimum": 18, "type": "integer", "format": "int32" - }, - "profile-image-url": { - "type": "string", - "format": "uri" } }, "additionalProperties": false @@ -2243,6 +2222,10 @@ "profile-image-url": { "type": "string", "format": "uri" + }, + "distance-traveled-in-kilometers": { + "type": "integer", + "format": "int64" } }, "additionalProperties": false @@ -2504,6 +2487,9 @@ "type": "string", "nullable": true }, + "operated-by": { + "$ref": "#/components/schemas/airline" + }, "departs-at": { "type": "string", "format": "date-time", @@ -2514,9 +2500,6 @@ "format": "date-time", "nullable": true }, - "operated-by": { - "$ref": "#/components/schemas/airline" - }, "services-on-board": { "type": "array", "items": { @@ -2712,13 +2695,13 @@ "flight-relationships-in-patch-request": { "type": "object", "properties": { + "cabin-personnel": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + }, "operating-airplane": { "$ref": "#/components/schemas/to-one-airplane-request-data" }, - "flight-attendants": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" - }, - "reserve-flight-attendants": { + "backup-cabin-personnel": { "$ref": "#/components/schemas/to-many-flight-attendant-request-data" } }, @@ -2727,13 +2710,13 @@ "flight-relationships-in-post-request": { "type": "object", "properties": { + "cabin-personnel": { + "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + }, "operating-airplane": { "$ref": "#/components/schemas/to-one-airplane-request-data" }, - "flight-attendants": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" - }, - "reserve-flight-attendants": { + "backup-cabin-personnel": { "$ref": "#/components/schemas/to-many-flight-attendant-request-data" } }, @@ -2742,13 +2725,13 @@ "flight-relationships-in-response": { "type": "object", "properties": { + "cabin-personnel": { + "$ref": "#/components/schemas/to-many-flight-attendant-response-data" + }, "operating-airplane": { "$ref": "#/components/schemas/to-one-airplane-response-data" }, - "flight-attendants": { - "$ref": "#/components/schemas/to-many-flight-attendant-response-data" - }, - "reserve-flight-attendants": { + "backup-cabin-personnel": { "$ref": "#/components/schemas/to-many-flight-attendant-response-data" } }, From aaf59a62f710bbc0b5792005acb250c0e1bad32f Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 16 Sep 2021 16:32:38 +0200 Subject: [PATCH 23/35] process review feedback --- .../client-library.md => openapi-client.md} | 4 +- .../api-description.md => openapi.md} | 5 +- docs/usage/toc.md | 5 +- .../JsonApiDotNetCore.OpenApi.Client.csproj | 3 +- ...lientAttributeRegistrationLifeTimeTests.cs | 66 +++++++++---------- .../LegacyClient/FakeHttpClientWrapper.cs | 1 - .../{ILegacyClient.cs => IOpenApiClient.cs} | 2 +- .../{LegacyClient.cs => OpenApiClient.cs} | 2 +- .../LegacyClient/RequestTests.cs | 60 ++++++++--------- .../LegacyClient/ResponseTests.cs | 63 +++++++++--------- .../OpenApiClientTests.csproj | 2 +- 11 files changed, 106 insertions(+), 107 deletions(-) rename docs/usage/{openapi/client-library.md => openapi-client.md} (88%) rename docs/usage/{openapi/api-description.md => openapi.md} (77%) rename test/OpenApiClientTests/LegacyClient/GeneratedCode/{ILegacyClient.cs => IOpenApiClient.cs} (72%) rename test/OpenApiClientTests/LegacyClient/GeneratedCode/{LegacyClient.cs => OpenApiClient.cs} (85%) diff --git a/docs/usage/openapi/client-library.md b/docs/usage/openapi-client.md similarity index 88% rename from docs/usage/openapi/client-library.md rename to docs/usage/openapi-client.md index 520d13e1e9..818acd8974 100644 --- a/docs/usage/openapi/client-library.md +++ b/docs/usage/openapi-client.md @@ -1,6 +1,6 @@ -# OpenAPI Client Library +# OpenAPI Client -You can generate a client library in various programming languages from the OpenAPI specification file that JsonApiDotNetCore APIs provide. For C# .NET clients generated using NSwag, we provide an additional package that introduces better support for PATCH and POST requests. The issue here is that a property on a generated C# class being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". +You can generate a client in various programming languages from the OpenAPI specification file that JsonApiDotNetCore APIs provide. For C# .NET clients generated using NSwag, we provide an additional package that introduces support for partial PATCH/POST requests. The issue here is that a property on a generated C# class being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". ## Installation diff --git a/docs/usage/openapi/api-description.md b/docs/usage/openapi.md similarity index 77% rename from docs/usage/openapi/api-description.md rename to docs/usage/openapi.md index 85bc0f62cb..153a35883f 100644 --- a/docs/usage/openapi/api-description.md +++ b/docs/usage/openapi.md @@ -1,6 +1,7 @@ -# OpenAPI API Description +# OpenAPI + +JsonApiDotNetCore provides an extension package that enables you to produce an [OpenAPI specification](https://swagger.io/specification/) for your JSON:API endpoints. This can be used to generate a [documentation website](https://swagger.io/tools/swagger-ui/) or to generate [client libraries](https://openapi-generator.tech/docs/generators/) in various languages. The package provides an integration with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore). -You can describe your API with an OpenAPI specification using the [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) integration for JsonApiDotNetCore. ## Installation diff --git a/docs/usage/toc.md b/docs/usage/toc.md index c3ce5513f2..0428c859ea 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -22,9 +22,8 @@ # [Metadata](meta.md) # [Caching](caching.md) -# OpenAPI -## [API Description](openapi/api-description.md)) -### [Client Library](openapi/client-library.md)) +# [OpenAPI](openapi.md)) +## [Client](openapi-client.md)) # Extensibility ## [Layer Overview](extensibility/layer-overview.md) diff --git a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj index e5d66557fb..8fdd3d9021 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj +++ b/src/JsonApiDotNetCore.OpenApi.Client/JsonApiDotNetCore.OpenApi.Client.csproj @@ -7,7 +7,7 @@ jsonapidotnetcore;jsonapi;json:api;dotnet;asp.net;openapi;swagger;client;nswag - Provides support for OpenAPI generated clients in sending partial POST/PATCH requests against JSON:API endpoints + Provides support for OpenAPI generated clients in sending partial POST/PATCH requests against JSON:API endpoints. json-api-dotnet https://www.jsonapi.net/ MIT @@ -26,5 +26,4 @@ - diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs index 7cd175baa7..6e9e6ff6f6 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs @@ -16,7 +16,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -30,16 +30,16 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } wrapper.ChangeResponse(HttpStatusCode.NoContent, null); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); // Assert wrapper.RequestBody.Should().BeJson(@"{ @@ -58,7 +58,7 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -75,17 +75,17 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); wrapper.ChangeResponse(HttpStatusCode.NoContent, null); requestDocument.Data.Attributes.AirtimeInHours = null; // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -105,7 +105,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -131,16 +131,16 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.IsInMaintenance)) { } // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } // Assert @@ -160,7 +160,7 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -177,13 +177,13 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.IsInMaintenance)) { requestDocument.Data.Attributes.IsInMaintenance = false; // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -203,7 +203,7 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -226,14 +226,14 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.IsInMaintenance)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); } } @@ -254,7 +254,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -268,10 +268,10 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); } const string airplaneId2 = "DJy1u"; @@ -291,11 +291,11 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } // Assert @@ -316,7 +316,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument1 = new AirplanePostRequestDocument { @@ -327,10 +327,10 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PostAirplaneAsync(requestDocument1)); } const string airplaneId = "DJy1u"; @@ -350,11 +350,11 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument2)); } // Assert @@ -375,7 +375,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -401,14 +401,14 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.SerialNumber)) { - using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } } diff --git a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs b/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs index 33fbe4ed05..e5cfe6a687 100644 --- a/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs +++ b/test/OpenApiClientTests/LegacyClient/FakeHttpClientWrapper.cs @@ -9,7 +9,6 @@ using JsonApiDotNetCore.OpenApi.Client; namespace OpenApiClientTests.LegacyClient - { /// /// Enables to inject an outgoing response body and inspect the incoming request. diff --git a/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs b/test/OpenApiClientTests/LegacyClient/GeneratedCode/IOpenApiClient.cs similarity index 72% rename from test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs rename to test/OpenApiClientTests/LegacyClient/GeneratedCode/IOpenApiClient.cs index f207b8b19d..55cf1e855f 100644 --- a/test/OpenApiClientTests/LegacyClient/GeneratedCode/ILegacyClient.cs +++ b/test/OpenApiClientTests/LegacyClient/GeneratedCode/IOpenApiClient.cs @@ -3,7 +3,7 @@ namespace OpenApiClientTests.LegacyClient.GeneratedCode { // ReSharper disable once MemberCanBeInternal - public partial interface ILegacyClient : IJsonApiClient + public partial interface IOpenApiClient : IJsonApiClient { } } diff --git a/test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs b/test/OpenApiClientTests/LegacyClient/GeneratedCode/OpenApiClient.cs similarity index 85% rename from test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs rename to test/OpenApiClientTests/LegacyClient/GeneratedCode/OpenApiClient.cs index e6ba211021..500d1354d2 100644 --- a/test/OpenApiClientTests/LegacyClient/GeneratedCode/LegacyClient.cs +++ b/test/OpenApiClientTests/LegacyClient/GeneratedCode/OpenApiClient.cs @@ -3,7 +3,7 @@ namespace OpenApiClientTests.LegacyClient.GeneratedCode { - internal partial class LegacyClient : JsonApiClient + internal partial class OpenApiClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) { diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index a40f8e26f2..853b185b7c 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -22,10 +22,10 @@ public async Task Getting_resource_collection_produces_expected_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCollectionAsync()); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCollectionAsync()); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -41,10 +41,10 @@ public async Task Getting_resource_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -58,7 +58,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new FlightPostRequestDocument { @@ -73,7 +73,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ }; // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostFlightAsync(requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PostFlightAsync(requestDocument)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -100,7 +100,7 @@ public async Task Partial_posting_resource_produces_expected_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); const char euroSign = '\x20AC'; const char checkMark = '\x2713'; @@ -128,11 +128,11 @@ public async Task Partial_posting_resource_produces_expected_request() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PostAirplaneAsync(requestDocument)); } // Assert @@ -163,7 +163,7 @@ public async Task Partial_patching_resource_produces_expected_request() var lastServicedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new AirplanePatchRequestDocument { @@ -178,11 +178,11 @@ public async Task Partial_patching_resource_produces_expected_request() } }; - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.SerialNumber, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -213,10 +213,10 @@ public async Task Deleting_resource_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - await apiClient.DeleteFlightAsync(flightId); + await apiOpenApiClient.DeleteFlightAsync(flightId); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Delete); @@ -231,10 +231,10 @@ public async Task Getting_secondary_resource_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightOperatingAirplaneAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightOperatingAirplaneAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -250,10 +250,10 @@ public async Task Getting_secondary_resources_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCabinPersonnelAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCabinPersonnelAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -269,10 +269,10 @@ public async Task Getting_ToOne_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -288,7 +288,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToOneAirplaneRequestData { @@ -300,7 +300,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() }; // Act - await apiClient.PatchFlightOperatingAirplaneRelationshipAsync(flightId, requestDocument); + await apiOpenApiClient.PatchFlightOperatingAirplaneRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); @@ -324,10 +324,10 @@ public async Task Getting_ToMany_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCabinPersonnelRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCabinPersonnelRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -343,7 +343,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -363,7 +363,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() }; // Act - await apiClient.PostFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); + await apiOpenApiClient.PostFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Post); @@ -393,7 +393,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -413,7 +413,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() }; // Act - await apiClient.PatchFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); + await apiOpenApiClient.PatchFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); @@ -443,7 +443,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() const int flightId = 8712; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -463,7 +463,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() }; // Act - await apiClient.DeleteFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); + await apiOpenApiClient.DeleteFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Delete); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 5d48507dd6..a1755d837b 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -98,10 +98,10 @@ public async Task Getting_resource_collection_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightCollectionResponseDocument document = await apiClient.GetFlightCollectionAsync(); + FlightCollectionResponseDocument document = await apiOpenApiClient.GetFlightCollectionAsync(); // Assert document.Jsonapi.Should().BeNull(); @@ -173,10 +173,10 @@ public async Task Getting_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await apiClient.GetFlightAsync(Convert.ToInt32(flightId)); + FlightPrimaryResponseDocument document = await apiOpenApiClient.GetFlightAsync(Convert.ToInt32(flightId)); // Assert document.Jsonapi.Should().BeNull(); @@ -208,10 +208,10 @@ public async Task Getting_unknown_resource_translates_error_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NotFound, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func> action = async () => await apiClient.GetFlightAsync(Convert.ToInt32(flightId)); + Func> action = async () => await apiOpenApiClient.GetFlightAsync(Convert.ToInt32(flightId)); // Assert ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); @@ -270,10 +270,10 @@ public async Task Posting_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.Created, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await apiClient.PostFlightAsync(new FlightPostRequestDocument + FlightPrimaryResponseDocument document = await apiOpenApiClient.PostFlightAsync(new FlightPostRequestDocument { Data = new FlightDataInPostRequest { @@ -321,10 +321,10 @@ public async Task Patching_resource_with_side_effects_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await apiClient.PatchFlightAsync(Convert.ToInt32(flightId), new FlightPatchRequestDocument + FlightPrimaryResponseDocument document = await apiOpenApiClient.PatchFlightAsync(Convert.ToInt32(flightId), new FlightPatchRequestDocument { Data = new FlightDataInPatchRequest { @@ -345,11 +345,11 @@ public async Task Patching_resource_without_side_effects_translates_response() // Arrange const string flightId = "8712"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await apiClient.PatchFlightAsync(Convert.ToInt32(flightId), - new FlightPatchRequestDocument + FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchFlightAsync( + Convert.ToInt32(flightId), new FlightPatchRequestDocument { Data = new FlightDataInPatchRequest { @@ -367,10 +367,10 @@ public async Task Deleting_resource_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.DeleteFlightAsync(8712); + Func action = async () => await apiOpenApiClient.DeleteFlightAsync(8712); // Assert await action.Should().NotThrowAsync(); @@ -392,10 +392,10 @@ public async Task Getting_secondary_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - AirplaneSecondaryResponseDocument document = await apiClient.GetFlightOperatingAirplaneAsync(Convert.ToInt32(flightId)); + AirplaneSecondaryResponseDocument document = await apiOpenApiClient.GetFlightOperatingAirplaneAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().BeNull(); @@ -416,10 +416,10 @@ public async Task Getting_secondary_resources_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantCollectionResponseDocument document = await apiClient.GetFlightCabinPersonnelAsync(Convert.ToInt32(flightId)); + FlightAttendantCollectionResponseDocument document = await apiOpenApiClient.GetFlightCabinPersonnelAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().BeEmpty(); @@ -444,10 +444,10 @@ public async Task Getting_ToOne_relationship_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - AirplaneIdentifierResponseDocument document = await apiClient.GetFlightOperatingAirplaneRelationshipAsync(Convert.ToInt32(flightId)); + AirplaneIdentifierResponseDocument document = await apiOpenApiClient.GetFlightOperatingAirplaneRelationshipAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().NotBeNull(); @@ -460,10 +460,10 @@ public async Task Patching_ToOne_relationship_translates_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - await apiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData + await apiOpenApiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData { Data = new AirplaneIdentifier { @@ -498,10 +498,11 @@ public async Task Getting_ToMany_relationship_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantIdentifierCollectionResponseDocument document = await apiClient.GetFlightCabinPersonnelRelationshipAsync(Convert.ToInt32(flightId)); + FlightAttendantIdentifierCollectionResponseDocument document = + await apiOpenApiClient.GetFlightCabinPersonnelRelationshipAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().HaveCount(2); @@ -516,10 +517,10 @@ public async Task Posting_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.PostFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await apiOpenApiClient.PostFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { @@ -545,10 +546,10 @@ public async Task Patching_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.PatchFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await apiOpenApiClient.PatchFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { @@ -574,10 +575,10 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - ILegacyClient apiClient = new GeneratedCode.LegacyClient(wrapper.HttpClient); + IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiClient.DeleteFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await apiOpenApiClient.DeleteFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj index bf3a11d666..d7e9c47166 100644 --- a/test/OpenApiClientTests/OpenApiClientTests.csproj +++ b/test/OpenApiClientTests/OpenApiClientTests.csproj @@ -30,7 +30,7 @@ OpenApiClientTests.LegacyClient.GeneratedCode - LegacyClient + LegacyOpenApiClient NSwagCSharp /UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal From 1baed2d3e305507cc10b0a086a2b0296094ac602 Mon Sep 17 00:00:00 2001 From: maurei Date: Thu, 16 Sep 2021 16:50:30 +0200 Subject: [PATCH 24/35] rename ClassName in client test project --- test/OpenApiClientTests/OpenApiClientTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenApiClientTests/OpenApiClientTests.csproj b/test/OpenApiClientTests/OpenApiClientTests.csproj index d7e9c47166..3bd3b5da2d 100644 --- a/test/OpenApiClientTests/OpenApiClientTests.csproj +++ b/test/OpenApiClientTests/OpenApiClientTests.csproj @@ -30,7 +30,7 @@ OpenApiClientTests.LegacyClient.GeneratedCode - LegacyOpenApiClient + OpenApiClient NSwagCSharp /UseBaseUrl:false /GenerateClientInterfaces:true /ClientClassAccessModifier:internal From 65430a87957fbb78a6b59e5926aafd9972adbb54 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 16 Sep 2021 19:46:42 +0200 Subject: [PATCH 25/35] Updated example client project using Visual Studio defaults and updated documentation --- docs/usage/openapi-client.md | 208 ++++++++---------- .../{GeneratedCode => }/ExampleApiClient.cs | 4 +- .../JsonApiDotNetCoreExampleClient.csproj | 53 +++-- .../{ => OpenAPIs}/swagger.json | 0 .../JsonApiDotNetCoreExampleClient/Program.cs | 12 +- 5 files changed, 129 insertions(+), 148 deletions(-) rename src/Examples/JsonApiDotNetCoreExampleClient/{GeneratedCode => }/ExampleApiClient.cs (71%) rename src/Examples/JsonApiDotNetCoreExampleClient/{ => OpenAPIs}/swagger.json (100%) diff --git a/docs/usage/openapi-client.md b/docs/usage/openapi-client.md index 818acd8974..221c1cd7f9 100644 --- a/docs/usage/openapi-client.md +++ b/docs/usage/openapi-client.md @@ -1,137 +1,123 @@ # OpenAPI Client -You can generate a client in various programming languages from the OpenAPI specification file that JsonApiDotNetCore APIs provide. For C# .NET clients generated using NSwag, we provide an additional package that introduces support for partial PATCH/POST requests. The issue here is that a property on a generated C# class being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". +You can generate a JSON:API client in various programming languages from the OpenAPI specification file that JsonApiDotNetCore APIs provide. For C# .NET clients generated using NSwag, we provide an additional package that introduces support for partial PATCH/POST requests. The issue here is that a property on a generated C# class being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". -## Installation - -You are required to install the following NuGet packages: - -- `JsonApiDotNetCore.OpenApi.Client` -- `NSwag.ApiDescription.Client` -- `Microsoft.Extensions.ApiDescription.Cient` -- `NSwag.ApiDescription.Client` - -The following examples demonstrate how to install the `JsonApiDotNetCore.OpenApi.Client` package. - -### CLI - -``` -dotnet add package JsonApiDotNetCore.OpenApi.Client -``` +## Getting started ### Visual Studio -```powershell -Install-Package JsonApiDotNetCore.OpenApi.Client -``` - -### *.csproj - -```xml - - - - -``` +The easiest way to get started is by using the built-in capabilities of Visual Studio. The next steps describe how to generate a JSON:API client library and use our package. +1. In **Solution Explorer**, right-click your client project, select **Add** > **Service Reference** and choose **OpenAPI**. +2. On the next page, specify the OpenAPI URL to your JSON:API server, for example: `http://localhost:14140/swagger/v1/swagger.json`. + Optionally provide a class name and namespace and click **Finish**. + Visual Studio now downloads your swagger.json and updates your project file. This results in a pre-build step that generates the client code. + Tip: To later re-download swagger.json and regenerate the client code, right-click **Dependencies** > **Manage Connected Services** and click the **Refresh** icon. +3. Although not strictly required, we recommend to run package update now, which fixes some issues and removes the `Stream` parameter from generated calls. +4. Add some demo code that calls one of your JSON:API endpoints. For example: + ```c# + using var httpClient = new HttpClient(); + var apiClient = new ExampleApiClient("http://localhost:14140", httpClient); -## Adding an OpenApiReference + PersonCollectionResponseDocument getResponse = await apiClient.GetPersonCollectionAsync(); -Add a reference to your OpenAPI specification in your project file as demonstrated below. - -```xml - - - JsonApiDotNetCoreExampleClient.GeneratedCode - ExampleApiClient - NSwagCSharp - /UseBaseUrl:false /GenerateClientInterfaces:true - - -``` - - -## Usage - -The NSwag tooling generates the OpenAPI client during a prebuild step. Once your application is built, -you can instantiate it using the class name as indicated in the project file. - -```c# -namespace JsonApiDotNetCoreExampleClient -{ - class Program + foreach (PersonDataInResponse person in getResponse.Data) { - static void Main(string[] args) - { - using (HttpClient httpClient = new HttpClient()) - { - ExampleApiClient exampleApiClient = new ExampleApiClient(httpClient); - - // IntelliSense is now available on `exampleApiClient`! - } - } + Console.WriteLine($"Found user {person.Id} named '{person.Attributes.FirstName} {person.Attributes.LastName}'."); } -} -``` - -Support for partial write requests can be enabled by leveraging the extensibility points of the generated client. - -```c# -namespace JsonApiDotNetCoreExampleClient.GeneratedCode -{ - // Note that this class should be in the same namespace as the ExampleApiClient generated by NSwag. - public partial class ExampleApiClient : JsonApiClient + ``` +5. Add our client package to your project: + ``` + dotnet add package JsonApiDotNetCore.OpenApi.Client + ``` +6. Add the following glue code to connect our package with your generated code. The code below assumes you specified `ExampleApiClient` as class name in step 2. + ```c# + using JsonApiDotNetCore.OpenApi.Client; + using Newtonsoft.Json; + + partial class ExampleApiClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) { SetSerializerSettingsForJsonApi(settings); } } -} -``` - -You can now perform a write request by calling the `RegisterAttributesForRequest` method. 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 the `alwaysIncludedAttributeSelectors` parameter. - -```c# -// Program.cs -static void Main(string[] args) -{ - using (HttpClient httpClient = new HttpClient()) + ``` +7. Extend your demo code to send a partial PATCH request with the help of our package: + ```c# + var patchRequest = new PersonPatchRequestDocument { - ExampleApiClient exampleApiClient = new ExampleApiClient(httpClient); - - var requestDocument = new PersonPatchRequestDocument + Data = new PersonDataInPatchRequest { - Data = new PersonDataInPatchRequest + Id = "1", + Attributes = new PersonAttributesInPatchRequest { - Id = "546", - Type = PersonResourceType.People, - Attributes = new PersonAttributesInPatchRequest - { - FirstName = "Jack" - } + FirstName = "Jack" } - }; - - using (apiClient.RegisterAttributesForRequestDocument(requestDocument, person => person.LastName) - { - await exampleApiClient.PatchPersonAsync(543, requestDocument)); - - // The request will look like this: - // - // { - // "data": { - // "type": "people", - // "id": "543", - // "attributes": { - // "firstName": "Jack", - // "lastName": null, - // } - // } - // } } + }; + // This line results in sending "lastName: null" instead of omitting it. + using (apiClient.RegisterAttributesForRequestDocument( + patchRequest, person => person.LastName)) + { + PersonPrimaryResponseDocument patchResponse = await apiClient.PatchPersonAsync("1", patchRequest); + + // The sent request looks like this: + // { + // "data": { + // "type": "people", + // "id": "1", + // "attributes": { + // "firstName": "Jack", + // "lastName": null + // } + // } + // } } -} + ``` + +### Other IDEs + +When using the command-line, you can try the [Microsoft.dotnet-openapi Global Tool](https://docs.microsoft.com/en-us/aspnet/core/web-api/microsoft.dotnet-openapi?view=aspnetcore-5.0). + +Alternatively, the next section shows what to add to your client project file directly: + +```xml + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + http://localhost:14140/swagger/v1/swagger.json + + ``` +From here, continue from step 3 in the list of steps for Visual Studio. + +## Customization + +### NSwga + +The `OpenApiReference` element in the project file accepts an `Options` element to pass additional settings to the client generator, +which are listed at https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs. + +For example, the next section puts the generated code in a namespace, removes the `baseUrl` parameter and generates an interface (which is handy for dependency injection): +```xml + + ExampleProject.ServiceAccess.GeneratedCode + SalesApiClient + NSwagCSharp + /UseBaseUrl:false /GenerateClientInterfaces:true + +``` diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/ExampleApiClient.cs similarity index 71% rename from src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs rename to src/Examples/JsonApiDotNetCoreExampleClient/ExampleApiClient.cs index 02950157e7..52878e55e3 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/GeneratedCode/ExampleApiClient.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/ExampleApiClient.cs @@ -1,9 +1,9 @@ using JsonApiDotNetCore.OpenApi.Client; using Newtonsoft.Json; -namespace JsonApiDotNetCoreExampleClient.GeneratedCode +namespace JsonApiDotNetCoreExampleClient { - internal partial class ExampleApiClient : JsonApiClient + public partial class ExampleApiClient : JsonApiClient { partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings) { diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj index 79544c38cf..b736fd0859 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj +++ b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj @@ -1,31 +1,28 @@ - - - Exe - $(NetCoreAppVersion) - + + + Exe + $(NetCoreAppVersion) + - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - ExampleApiClient - NSwagCSharp - JsonApiDotNetCoreExampleClient.GeneratedCode - /UseBaseUrl:false /ClientClassAccessModifier:internal - - + + + - - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + http://localhost:14140/swagger/v1/swagger.json + + diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/swagger.json b/src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json similarity index 100% rename from src/Examples/JsonApiDotNetCoreExampleClient/swagger.json rename to src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs index acf5182cc4..a4a8b6ff08 100644 --- a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs +++ b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs @@ -1,20 +1,18 @@ -using System; +using System; using System.Net.Http; using System.Threading.Tasks; -using JsonApiDotNetCoreExampleClient.GeneratedCode; namespace JsonApiDotNetCoreExampleClient { internal static class Program { + private const string BaseUrl = "http://localhost:14140"; + private static async Task Main() { - using var httpClient = new HttpClient - { - BaseAddress = new Uri("http://localhost:14140") - }; + using var httpClient = new HttpClient(); - ExampleApiClient exampleApiClient = new(httpClient); + ExampleApiClient exampleApiClient = new(BaseUrl, httpClient); try { From 520c618816207c6440a3e7f7bf9082fdc5ef1075 Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Thu, 16 Sep 2021 20:12:57 +0200 Subject: [PATCH 26/35] Layout tweaks and fixes --- docs/usage/openapi-client.md | 36 +++++++++++++----- docs/usage/openapi.md | 72 ++++++++++++++---------------------- docs/usage/toc.md | 4 +- 3 files changed, 56 insertions(+), 56 deletions(-) diff --git a/docs/usage/openapi-client.md b/docs/usage/openapi-client.md index 221c1cd7f9..7250bb55ce 100644 --- a/docs/usage/openapi-client.md +++ b/docs/usage/openapi-client.md @@ -1,6 +1,8 @@ # OpenAPI Client -You can generate a JSON:API client in various programming languages from the OpenAPI specification file that JsonApiDotNetCore APIs provide. For C# .NET clients generated using NSwag, we provide an additional package that introduces support for partial PATCH/POST requests. The issue here is that a property on a generated C# class being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". +You can generate a JSON:API client in various programming languages from the [OpenAPI specification](https://swagger.io/specification/) file that JsonApiDotNetCore APIs provide. + +For C# .NET clients generated using [NSwag](https://github.com/RicoSuter/NSwag), we provide an additional package that introduces support for partial PATCH/POST requests. The issue here is that a property on a generated C# class being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it". ## Getting started @@ -9,28 +11,38 @@ You can generate a JSON:API client in various programming languages from the Ope The easiest way to get started is by using the built-in capabilities of Visual Studio. The next steps describe how to generate a JSON:API client library and use our package. 1. In **Solution Explorer**, right-click your client project, select **Add** > **Service Reference** and choose **OpenAPI**. + 2. On the next page, specify the OpenAPI URL to your JSON:API server, for example: `http://localhost:14140/swagger/v1/swagger.json`. Optionally provide a class name and namespace and click **Finish**. Visual Studio now downloads your swagger.json and updates your project file. This results in a pre-build step that generates the client code. + Tip: To later re-download swagger.json and regenerate the client code, right-click **Dependencies** > **Manage Connected Services** and click the **Refresh** icon. 3. Although not strictly required, we recommend to run package update now, which fixes some issues and removes the `Stream` parameter from generated calls. + 4. Add some demo code that calls one of your JSON:API endpoints. For example: + ```c# using var httpClient = new HttpClient(); var apiClient = new ExampleApiClient("http://localhost:14140", httpClient); - PersonCollectionResponseDocument getResponse = await apiClient.GetPersonCollectionAsync(); + PersonCollectionResponseDocument getResponse = + await apiClient.GetPersonCollectionAsync(); foreach (PersonDataInResponse person in getResponse.Data) { - Console.WriteLine($"Found user {person.Id} named '{person.Attributes.FirstName} {person.Attributes.LastName}'."); + Console.WriteLine($"Found user {person.Id} named " + + $"'{person.Attributes.FirstName} {person.Attributes.LastName}'."); } ``` + 5. Add our client package to your project: + ``` dotnet add package JsonApiDotNetCore.OpenApi.Client ``` + 6. Add the following glue code to connect our package with your generated code. The code below assumes you specified `ExampleApiClient` as class name in step 2. + ```c# using JsonApiDotNetCore.OpenApi.Client; using Newtonsoft.Json; @@ -43,7 +55,9 @@ The easiest way to get started is by using the built-in capabilities of Visual S } } ``` + 7. Extend your demo code to send a partial PATCH request with the help of our package: + ```c# var patchRequest = new PersonPatchRequestDocument { @@ -58,10 +72,11 @@ The easiest way to get started is by using the built-in capabilities of Visual S }; // This line results in sending "lastName: null" instead of omitting it. - using (apiClient.RegisterAttributesForRequestDocument( - patchRequest, person => person.LastName)) + using (apiClient.RegisterAttributesForRequestDocument(patchRequest, person => person.LastName)) { - PersonPrimaryResponseDocument patchResponse = await apiClient.PatchPersonAsync("1", patchRequest); + PersonPrimaryResponseDocument patchResponse = + await apiClient.PatchPersonAsync("1", patchRequest); // The sent request looks like this: // { @@ -105,17 +120,18 @@ Alternatively, the next section shows what to add to your client project file di From here, continue from step 3 in the list of steps for Visual Studio. -## Customization +## Configuration -### NSwga +### NSwag The `OpenApiReference` element in the project file accepts an `Options` element to pass additional settings to the client generator, -which are listed at https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs. +which are listed [here](https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs). For example, the next section puts the generated code in a namespace, removes the `baseUrl` parameter and generates an interface (which is handy for dependency injection): + ```xml - ExampleProject.ServiceAccess.GeneratedCode + ExampleProject.GeneratedCode SalesApiClient NSwagCSharp /UseBaseUrl:false /GenerateClientInterfaces:true diff --git a/docs/usage/openapi.md b/docs/usage/openapi.md index 153a35883f..07e731dce7 100644 --- a/docs/usage/openapi.md +++ b/docs/usage/openapi.md @@ -3,63 +3,47 @@ JsonApiDotNetCore provides an extension package that enables you to produce an [OpenAPI specification](https://swagger.io/specification/) for your JSON:API endpoints. This can be used to generate a [documentation website](https://swagger.io/tools/swagger-ui/) or to generate [client libraries](https://openapi-generator.tech/docs/generators/) in various languages. The package provides an integration with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore). -## Installation +## Getting started -Install the `JsonApiDotNetCore.OpenApi` NuGet package. +1. Install the `JsonApiDotNetCore.OpenApi` NuGet package: -### CLI + ``` + dotnet add package JsonApiDotNetCore.OpenApi + ``` -``` -dotnet add package JsonApiDotNetCore.OpenApi -``` - -### Visual Studio - -```powershell -Install-Package JsonApiDotNetCore.OpenApi -``` - -### *.csproj - -```xml - - - - -``` - -## Usage +2. Add the integration in your `Startup` class. -Add the integration in your `Startup` class. - -```c# -public class Startup -{ - public void ConfigureServices(IServiceCollection services) + ```c# + public class Startup { - IMvcCoreBuilder mvcBuilder = services.AddMvcCore(); - services.AddJsonApi(mvcBuilder: mvcBuilder); + public void ConfigureServices(IServiceCollection services) + { + IMvcCoreBuilder mvcBuilder = services.AddMvcCore(); - // Adds the Swashbuckle integration. - services.AddOpenApi(mvcBuilder); - } + services.AddJsonApi(mvcBuilder: mvcBuilder); - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - app.UseRouting(); - app.UseJsonApi(); + // Adds the Swashbuckle integration. + services.AddOpenApi(mvcBuilder); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + app.UseJsonApi(); - // Adds the Swashbuckle middleware. - app.UseSwagger(); + // Adds the Swashbuckle middleware. + app.UseSwagger(); - app.UseEndpoints(endpoints => endpoints.MapControllers()); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + } } -} -``` + ``` By default, the OpenAPI specification will be available at `http://localhost:/swagger/v1/swagger.json`. -Swashbuckle also ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), tooling for a generated documentation page. This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Startup` class. +## Documentation + +Swashbuckle also ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), tooling for a generated documentation page. This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Startup` class: ```c# // Startup.cs diff --git a/docs/usage/toc.md b/docs/usage/toc.md index 0428c859ea..10fee6bc72 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -22,8 +22,8 @@ # [Metadata](meta.md) # [Caching](caching.md) -# [OpenAPI](openapi.md)) -## [Client](openapi-client.md)) +# [OpenAPI](openapi.md) +## [Client](openapi-client.md) # Extensibility ## [Layer Overview](extensibility/layer-overview.md) From b72d82357046bee4357791146f58e6d8285aa6d9 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 11:02:02 +0200 Subject: [PATCH 27/35] fixed failed automatic rename 'openApiOpenApi' --- ...lientAttributeRegistrationLifeTimeTests.cs | 66 +++++++++---------- .../LegacyClient/RequestTests.cs | 6 +- .../LegacyClient/ResponseTests.cs | 60 ++++++++--------- 3 files changed, 66 insertions(+), 66 deletions(-) diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs index 6e9e6ff6f6..3d167507b3 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs @@ -16,7 +16,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -30,16 +30,16 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } wrapper.ChangeResponse(HttpStatusCode.NoContent, null); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); // Assert wrapper.RequestBody.Should().BeJson(@"{ @@ -58,7 +58,7 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -75,17 +75,17 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); wrapper.ChangeResponse(HttpStatusCode.NoContent, null); requestDocument.Data.Attributes.AirtimeInHours = null; // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -105,7 +105,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -131,16 +131,16 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.IsInMaintenance)) { } // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } // Assert @@ -160,7 +160,7 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -177,13 +177,13 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.IsInMaintenance)) { requestDocument.Data.Attributes.IsInMaintenance = false; // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -203,7 +203,7 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -226,14 +226,14 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.IsInMaintenance)) { - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); } } @@ -254,7 +254,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -268,10 +268,10 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); } const string airplaneId2 = "DJy1u"; @@ -291,11 +291,11 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } // Assert @@ -316,7 +316,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument1 = new AirplanePostRequestDocument { @@ -327,10 +327,10 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PostAirplaneAsync(requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PostAirplaneAsync(requestDocument1)); } const string airplaneId = "DJy1u"; @@ -350,11 +350,11 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument2)); } // Assert @@ -375,7 +375,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -401,14 +401,14 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.SerialNumber)) { - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } } diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 853b185b7c..1167131224 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -100,7 +100,7 @@ public async Task Partial_posting_resource_produces_expected_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); const char euroSign = '\x20AC'; const char checkMark = '\x2713'; @@ -128,11 +128,11 @@ public async Task Partial_posting_resource_produces_expected_request() } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PostAirplaneAsync(requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PostAirplaneAsync(requestDocument)); } // Assert diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index a1755d837b..6974f89f94 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -98,10 +98,10 @@ public async Task Getting_resource_collection_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightCollectionResponseDocument document = await apiOpenApiClient.GetFlightCollectionAsync(); + FlightCollectionResponseDocument document = await openApiClient.GetFlightCollectionAsync(); // Assert document.Jsonapi.Should().BeNull(); @@ -173,10 +173,10 @@ public async Task Getting_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await apiOpenApiClient.GetFlightAsync(Convert.ToInt32(flightId)); + FlightPrimaryResponseDocument document = await openApiClient.GetFlightAsync(Convert.ToInt32(flightId)); // Assert document.Jsonapi.Should().BeNull(); @@ -208,10 +208,10 @@ public async Task Getting_unknown_resource_translates_error_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NotFound, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func> action = async () => await apiOpenApiClient.GetFlightAsync(Convert.ToInt32(flightId)); + Func> action = async () => await openApiClient.GetFlightAsync(Convert.ToInt32(flightId)); // Assert ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); @@ -270,10 +270,10 @@ public async Task Posting_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.Created, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await apiOpenApiClient.PostFlightAsync(new FlightPostRequestDocument + FlightPrimaryResponseDocument document = await openApiClient.PostFlightAsync(new FlightPostRequestDocument { Data = new FlightDataInPostRequest { @@ -321,10 +321,10 @@ public async Task Patching_resource_with_side_effects_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await apiOpenApiClient.PatchFlightAsync(Convert.ToInt32(flightId), new FlightPatchRequestDocument + FlightPrimaryResponseDocument document = await openApiClient.PatchFlightAsync(Convert.ToInt32(flightId), new FlightPatchRequestDocument { Data = new FlightDataInPatchRequest { @@ -345,10 +345,10 @@ public async Task Patching_resource_without_side_effects_translates_response() // Arrange const string flightId = "8712"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchFlightAsync( + FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchFlightAsync( Convert.ToInt32(flightId), new FlightPatchRequestDocument { Data = new FlightDataInPatchRequest @@ -367,10 +367,10 @@ public async Task Deleting_resource_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiOpenApiClient.DeleteFlightAsync(8712); + Func action = async () => await openApiClient.DeleteFlightAsync(8712); // Assert await action.Should().NotThrowAsync(); @@ -392,10 +392,10 @@ public async Task Getting_secondary_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - AirplaneSecondaryResponseDocument document = await apiOpenApiClient.GetFlightOperatingAirplaneAsync(Convert.ToInt32(flightId)); + AirplaneSecondaryResponseDocument document = await openApiClient.GetFlightOperatingAirplaneAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().BeNull(); @@ -416,10 +416,10 @@ public async Task Getting_secondary_resources_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantCollectionResponseDocument document = await apiOpenApiClient.GetFlightCabinPersonnelAsync(Convert.ToInt32(flightId)); + FlightAttendantCollectionResponseDocument document = await openApiClient.GetFlightCabinPersonnelAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().BeEmpty(); @@ -444,10 +444,10 @@ public async Task Getting_ToOne_relationship_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - AirplaneIdentifierResponseDocument document = await apiOpenApiClient.GetFlightOperatingAirplaneRelationshipAsync(Convert.ToInt32(flightId)); + AirplaneIdentifierResponseDocument document = await openApiClient.GetFlightOperatingAirplaneRelationshipAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().NotBeNull(); @@ -460,10 +460,10 @@ public async Task Patching_ToOne_relationship_translates_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - await apiOpenApiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData + await openApiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData { Data = new AirplaneIdentifier { @@ -498,11 +498,11 @@ public async Task Getting_ToMany_relationship_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act FlightAttendantIdentifierCollectionResponseDocument document = - await apiOpenApiClient.GetFlightCabinPersonnelRelationshipAsync(Convert.ToInt32(flightId)); + await openApiClient.GetFlightCabinPersonnelRelationshipAsync(Convert.ToInt32(flightId)); // Assert document.Data.Should().HaveCount(2); @@ -517,10 +517,10 @@ public async Task Posting_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiOpenApiClient.PostFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.PostFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { @@ -546,10 +546,10 @@ public async Task Patching_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiOpenApiClient.PatchFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.PatchFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { @@ -575,10 +575,10 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await apiOpenApiClient.DeleteFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.DeleteFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData { Data = new List { From 605d2ce0b47e398d72239e1e95173e69a345e8ec Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 11:15:40 +0200 Subject: [PATCH 28/35] reduced changed wrt private version of this test --- test/OpenApiClientTests/LegacyClient/RequestTests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 1167131224..0801fe680f 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -179,7 +179,8 @@ public async Task Partial_patching_resource_produces_expected_request() }; using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, - airplane => airplane.SerialNumber, airplane => airplane.AirtimeInHours)) + airplane => airplane.SerialNumber, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, + airplane => airplane.AirtimeInHours)) { // Act _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); @@ -200,7 +201,8 @@ public async Task Partial_patching_resource_produces_expected_request() ""attributes"": { ""serial-number"": null, ""airtime-in-hours"": null, - ""last-serviced-at"": ""2021-01-01T15:23:05.033+04:00"" + ""last-serviced-at"": ""2021-01-01T15:23:05.033+04:00"", + ""is-in-maintenance"": false } } }"); From 8c2a7b4035d86fa9a9374d4cb5d8cca574781886 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 11:23:06 +0200 Subject: [PATCH 29/35] Changed ID type from to for Flight --- .../LegacyClient/RequestTests.cs | 20 +++--- .../LegacyClient/ResponseTests.cs | 49 +++++++------ .../LegacyOpenApiIntegration/Flight.cs | 2 +- .../FlightsController.cs | 4 +- .../LegacyOpenApiIntegration/swagger.json | 69 +++++++------------ 5 files changed, 60 insertions(+), 84 deletions(-) diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 0801fe680f..37ec602e33 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -38,7 +38,7 @@ public async Task Getting_resource_collection_produces_expected_request() public async Task Getting_resource_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -212,7 +212,7 @@ public async Task Partial_patching_resource_produces_expected_request() public async Task Deleting_resource_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -230,7 +230,7 @@ public async Task Deleting_resource_produces_expected_request() public async Task Getting_secondary_resource_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -249,7 +249,7 @@ public async Task Getting_secondary_resource_produces_expected_request() public async Task Getting_secondary_resources_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -268,7 +268,7 @@ public async Task Getting_secondary_resources_produces_expected_request() public async Task Getting_ToOne_relationship_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -287,7 +287,7 @@ public async Task Getting_ToOne_relationship_produces_expected_request() public async Task Patching_ToOne_relationship_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -323,7 +323,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() public async Task Getting_ToMany_relationship_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -342,7 +342,7 @@ public async Task Getting_ToMany_relationship_produces_expected_request() public async Task Posting_ToMany_relationship_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -392,7 +392,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() public async Task Patching_ToMany_relationship_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); @@ -442,7 +442,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() public async Task Deleting_ToMany_relationship_produces_expected_request() { // Arrange - const int flightId = 8712; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 6974f89f94..c220ed500c 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -21,7 +21,7 @@ public sealed class ResponseTests public async Task Getting_resource_collection_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string flightDestination = "Destination of Flight"; const string fightServiceOnBoard = "Movies"; const string flightDepartsAt = "2014-11-25T00:00:00"; @@ -151,7 +151,7 @@ public async Task Getting_resource_collection_translates_response() public async Task Getting_resource_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string departsAtInZuluTime = "2021-06-08T12:53:30.554Z"; const string arrivesAtWithUtcOffset = "2019-02-20T11:56:33.0721266+01:00"; @@ -176,7 +176,7 @@ public async Task Getting_resource_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await openApiClient.GetFlightAsync(Convert.ToInt32(flightId)); + FlightPrimaryResponseDocument document = await openApiClient.GetFlightAsync(flightId); // Assert document.Jsonapi.Should().BeNull(); @@ -194,7 +194,7 @@ public async Task Getting_resource_translates_response() public async Task Getting_unknown_resource_translates_error_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string responseBody = @"{ ""errors"": [ @@ -211,7 +211,7 @@ public async Task Getting_unknown_resource_translates_error_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func> action = async () => await openApiClient.GetFlightAsync(Convert.ToInt32(flightId)); + Func> action = async () => await openApiClient.GetFlightAsync(flightId); // Assert ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); @@ -225,7 +225,7 @@ public async Task Getting_unknown_resource_translates_error_response() public async Task Posting_resource_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string flightAttendantId = "bBJHu"; const string responseBody = @"{ @@ -305,7 +305,7 @@ public async Task Posting_resource_translates_response() public async Task Patching_resource_with_side_effects_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string responseBody = @"{ ""links"": { @@ -324,7 +324,7 @@ public async Task Patching_resource_with_side_effects_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await openApiClient.PatchFlightAsync(Convert.ToInt32(flightId), new FlightPatchRequestDocument + FlightPrimaryResponseDocument document = await openApiClient.PatchFlightAsync(flightId, new FlightPatchRequestDocument { Data = new FlightDataInPatchRequest { @@ -343,13 +343,13 @@ public async Task Patching_resource_with_side_effects_translates_response() public async Task Patching_resource_without_side_effects_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchFlightAsync( - Convert.ToInt32(flightId), new FlightPatchRequestDocument + FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchFlightAsync(flightId, + new FlightPatchRequestDocument { Data = new FlightDataInPatchRequest { @@ -370,7 +370,7 @@ public async Task Deleting_resource_produces_empty_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.DeleteFlightAsync(8712); + Func action = async () => await openApiClient.DeleteFlightAsync("ZvuH1"); // Assert await action.Should().NotThrowAsync(); @@ -380,7 +380,7 @@ public async Task Deleting_resource_produces_empty_response() public async Task Getting_secondary_resource_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string responseBody = @"{ ""links"": { @@ -395,7 +395,7 @@ public async Task Getting_secondary_resource_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - AirplaneSecondaryResponseDocument document = await openApiClient.GetFlightOperatingAirplaneAsync(Convert.ToInt32(flightId)); + AirplaneSecondaryResponseDocument document = await openApiClient.GetFlightOperatingAirplaneAsync(flightId); // Assert document.Data.Should().BeNull(); @@ -405,7 +405,7 @@ public async Task Getting_secondary_resource_translates_response() public async Task Getting_secondary_resources_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string responseBody = @"{ ""links"": { @@ -419,7 +419,7 @@ public async Task Getting_secondary_resources_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantCollectionResponseDocument document = await openApiClient.GetFlightCabinPersonnelAsync(Convert.ToInt32(flightId)); + FlightAttendantCollectionResponseDocument document = await openApiClient.GetFlightCabinPersonnelAsync(flightId); // Assert document.Data.Should().BeEmpty(); @@ -429,7 +429,7 @@ public async Task Getting_secondary_resources_translates_response() public async Task Getting_ToOne_relationship_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string operatingAirplaneId = "bBJHu"; const string responseBody = @"{ @@ -447,7 +447,7 @@ public async Task Getting_ToOne_relationship_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - AirplaneIdentifierResponseDocument document = await openApiClient.GetFlightOperatingAirplaneRelationshipAsync(Convert.ToInt32(flightId)); + AirplaneIdentifierResponseDocument document = await openApiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId); // Assert document.Data.Should().NotBeNull(); @@ -463,7 +463,7 @@ public async Task Patching_ToOne_relationship_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - await openApiClient.PatchFlightOperatingAirplaneRelationshipAsync(8712, new ToOneAirplaneRequestData + await openApiClient.PatchFlightOperatingAirplaneRelationshipAsync("ZvuH1", new ToOneAirplaneRequestData { Data = new AirplaneIdentifier { @@ -477,7 +477,7 @@ public async Task Patching_ToOne_relationship_translates_response() public async Task Getting_ToMany_relationship_translates_response() { // Arrange - const string flightId = "8712"; + const string flightId = "ZvuH1"; const string flightAttendantId1 = "bBJHu"; const string flightAttendantId2 = "ZvuHNInmX1"; @@ -501,8 +501,7 @@ public async Task Getting_ToMany_relationship_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantIdentifierCollectionResponseDocument document = - await openApiClient.GetFlightCabinPersonnelRelationshipAsync(Convert.ToInt32(flightId)); + FlightAttendantIdentifierCollectionResponseDocument document = await openApiClient.GetFlightCabinPersonnelRelationshipAsync(flightId); // Assert document.Data.Should().HaveCount(2); @@ -520,7 +519,7 @@ public async Task Posting_ToMany_relationship_produces_empty_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.PostFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.PostFlightCabinPersonnelRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List { @@ -549,7 +548,7 @@ public async Task Patching_ToMany_relationship_produces_empty_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.PatchFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.PatchFlightCabinPersonnelRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List { @@ -578,7 +577,7 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.DeleteFlightCabinPersonnelRelationshipAsync(8712, new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.DeleteFlightCabinPersonnelRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List { diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs index 1082c991a0..2ed0d0fd1b 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs @@ -9,7 +9,7 @@ namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class Flight : Identifiable + public sealed class Flight : Identifiable { [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowChange)] [Required] diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/FlightsController.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightsController.cs index 0b764d052f..6d243c893c 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/FlightsController.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightsController.cs @@ -5,9 +5,9 @@ namespace OpenApiTests.LegacyOpenApiIntegration { - public sealed class FlightsController : JsonApiController + public sealed class FlightsController : JsonApiController { - public FlightsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + public FlightsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index 0fc85df9de..79cc427c58 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -1095,8 +1095,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1124,8 +1123,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1153,8 +1151,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1194,8 +1191,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1218,8 +1214,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1247,8 +1242,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1278,8 +1272,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1307,8 +1300,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1336,8 +1328,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1367,8 +1358,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1398,8 +1388,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1431,8 +1420,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1460,8 +1448,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1491,8 +1478,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1520,8 +1506,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1549,8 +1534,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1580,8 +1564,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1611,8 +1594,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1644,8 +1626,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1673,8 +1654,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1704,8 +1684,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1733,8 +1712,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], @@ -1762,8 +1740,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int32" + "type": "string" } } ], From a83de891bd4eea764f1ffafe5f828675961dced9 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 12:18:11 +0200 Subject: [PATCH 30/35] ReserveCabinPersonnel -> Purser, CabinPersonnel -> CabinCrewMembers --- .../LegacyClient/RequestTests.cs | 20 +- .../LegacyClient/ResponseTests.cs | 88 +++--- .../LegacyOpenApiIntegration/Flight.cs | 6 +- .../FlightAttendant.cs | 2 +- .../LegacyIntegrationDbContext.cs | 6 +- .../LegacyOpenApiIntegration/swagger.json | 291 ++++++++++-------- 6 files changed, 229 insertions(+), 184 deletions(-) diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 37ec602e33..43c645271a 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -255,12 +255,12 @@ public async Task Getting_secondary_resources_produces_expected_request() IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCabinPersonnelAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCabinCrewMembersAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/cabin-personnel"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/cabin-crew-members"); wrapper.RequestBody.Should().BeNull(); } @@ -329,12 +329,12 @@ public async Task Getting_ToMany_relationship_produces_expected_request() IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCabinPersonnelRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCabinCrewMembersRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-personnel"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-crew-members"); wrapper.RequestBody.Should().BeNull(); } @@ -365,11 +365,11 @@ public async Task Posting_ToMany_relationship_produces_expected_request() }; // Act - await apiOpenApiClient.PostFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); + await apiOpenApiClient.PostFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Post); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-personnel"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-crew-members"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -415,11 +415,11 @@ public async Task Patching_ToMany_relationship_produces_expected_request() }; // Act - await apiOpenApiClient.PatchFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); + await apiOpenApiClient.PatchFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-personnel"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-crew-members"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); @@ -465,11 +465,11 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() }; // Act - await apiOpenApiClient.DeleteFlightCabinPersonnelRelationshipAsync(flightId, requestDocument); + await apiOpenApiClient.DeleteFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Delete); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-personnel"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/cabin-crew-members"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index c220ed500c..6dc6d3e4c9 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -28,8 +28,8 @@ public async Task Getting_resource_collection_translates_response() const string documentMetaValue = "1"; const string flightMetaValue = "https://api.jsonapi.net/docs/#get-flights"; const string operatingAirplaneMetaValue = "https://jsonapi.net/api/docs/#get-flight-operating-airplane"; - const string cabinPersonnelMetaValue = "https://jsonapi.net/api/docs/#get-flight-cabin-personnel"; - const string reserveCabinPersonnelMetaValue = "https://jsonapi.net/api/docs/#get-flight-backup-cabin-personnel"; + const string cabinPersonnelMetaValue = "https://jsonapi.net/api/docs/#get-flight-cabin-crew-members"; + const string purserMetaValue = "https://jsonapi.net/api/docs/#get-flight-purser"; const string topLevelLink = HostPrefix + "flights"; const string flightResourceLink = topLevelLink + "/" + flightId; @@ -68,22 +68,22 @@ public async Task Getting_resource_collection_translates_response() ""docs"": """ + operatingAirplaneMetaValue + @""" } }, - ""cabin-personnel"": { + ""cabin-crew-members"": { ""links"": { - ""self"": """ + flightResourceLink + @"/relationships/cabin-personnel"", - ""related"": """ + flightResourceLink + @"/cabin-personnel"" + ""self"": """ + flightResourceLink + @"/relationships/cabin-crew-members"", + ""related"": """ + flightResourceLink + @"/cabin-crew-members"" }, ""meta"": { ""docs"": """ + cabinPersonnelMetaValue + @""" } }, - ""backup-cabin-personnel"": { + ""purser"": { ""links"": { - ""self"": """ + flightResourceLink + @"/relationships/backup-cabin-personnel"", - ""related"": """ + flightResourceLink + @"/backup-cabin-personnel"" + ""self"": """ + flightResourceLink + @"/relationships/purser"", + ""related"": """ + flightResourceLink + @"/purser"" }, ""meta"": { - ""docs"": """ + reserveCabinPersonnelMetaValue + @""" + ""docs"": """ + purserMetaValue + @""" } } }, @@ -134,17 +134,17 @@ public async Task Getting_resource_collection_translates_response() flight.Relationships.OperatingAirplane.Meta.Should().HaveCount(1); flight.Relationships.OperatingAirplane.Meta["docs"].Should().Be(operatingAirplaneMetaValue); - flight.Relationships.CabinPersonnel.Data.Should().BeNull(); - flight.Relationships.CabinPersonnel.Links.Self.Should().Be(flightResourceLink + "/relationships/cabin-personnel"); - flight.Relationships.CabinPersonnel.Links.Related.Should().Be(flightResourceLink + "/cabin-personnel"); - flight.Relationships.CabinPersonnel.Meta.Should().HaveCount(1); - flight.Relationships.CabinPersonnel.Meta["docs"].Should().Be(cabinPersonnelMetaValue); - - flight.Relationships.BackupCabinPersonnel.Data.Should().BeNull(); - flight.Relationships.BackupCabinPersonnel.Links.Self.Should().Be(flightResourceLink + "/relationships/backup-cabin-personnel"); - flight.Relationships.BackupCabinPersonnel.Links.Related.Should().Be(flightResourceLink + "/backup-cabin-personnel"); - flight.Relationships.BackupCabinPersonnel.Meta.Should().HaveCount(1); - flight.Relationships.BackupCabinPersonnel.Meta["docs"].Should().Be(reserveCabinPersonnelMetaValue); + flight.Relationships.CabinCrewMembers.Data.Should().BeNull(); + flight.Relationships.CabinCrewMembers.Links.Self.Should().Be(flightResourceLink + "/relationships/cabin-crew-members"); + flight.Relationships.CabinCrewMembers.Links.Related.Should().Be(flightResourceLink + "/cabin-crew-members"); + flight.Relationships.CabinCrewMembers.Meta.Should().HaveCount(1); + flight.Relationships.CabinCrewMembers.Meta["docs"].Should().Be(cabinPersonnelMetaValue); + + flight.Relationships.Purser.Data.Should().BeNull(); + flight.Relationships.Purser.Links.Self.Should().Be(flightResourceLink + "/relationships/purser"); + flight.Relationships.Purser.Links.Related.Should().Be(flightResourceLink + "/purser"); + flight.Relationships.Purser.Meta.Should().HaveCount(1); + flight.Relationships.Purser.Meta["docs"].Should().Be(purserMetaValue); } [Fact] @@ -230,7 +230,7 @@ public async Task Posting_resource_translates_response() const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-personnel,backup-cabin-personnel"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-crew-members,purser"" }, ""data"": { ""type"": ""flights"", @@ -243,10 +243,10 @@ public async Task Posting_resource_translates_response() }, ""data"": null }, - ""cabin-personnel"": { + ""cabin-crew-members"": { ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-personnel"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-personnel"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-crew-members"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-crew-members"" }, ""data"": [ { @@ -255,16 +255,16 @@ public async Task Posting_resource_translates_response() } ], }, - ""backup-cabin-personnel"": { + ""purser"": { ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/backup-cabin-personnel"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/backup-cabin-personnel"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/purser"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/purser"" }, - ""data"": [ ] + ""data"": null } }, ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-personnel,backup-cabin-personnel"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-crew-members,purser"" } } }"; @@ -295,10 +295,10 @@ public async Task Posting_resource_translates_response() // Assert document.Data.Attributes.Should().BeNull(); document.Data.Relationships.OperatingAirplane.Data.Should().BeNull(); - document.Data.Relationships.CabinPersonnel.Data.Should().HaveCount(1); - document.Data.Relationships.CabinPersonnel.Data.First().Id.Should().Be(flightAttendantId); - document.Data.Relationships.CabinPersonnel.Data.First().Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); - document.Data.Relationships.BackupCabinPersonnel.Data.Should().BeEmpty(); + document.Data.Relationships.CabinCrewMembers.Data.Should().HaveCount(1); + document.Data.Relationships.CabinCrewMembers.Data.First().Id.Should().Be(flightAttendantId); + document.Data.Relationships.CabinCrewMembers.Data.First().Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); + document.Data.Relationships.Purser.Data.Should().BeNull(); } [Fact] @@ -315,7 +315,7 @@ public async Task Patching_resource_with_side_effects_translates_response() ""type"": ""flights"", ""id"": """ + flightId + @""", ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-personnel,backup-cabin-personnel"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-crew-members,purser"" } } }"; @@ -409,8 +409,8 @@ public async Task Getting_secondary_resources_translates_response() const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-personnel"", - ""first"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-personnel"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-crew-members"", + ""first"": """ + HostPrefix + @"flights/" + flightId + @"/cabin-crew-members"" }, ""data"": [ ] }"; @@ -419,7 +419,7 @@ public async Task Getting_secondary_resources_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantCollectionResponseDocument document = await openApiClient.GetFlightCabinPersonnelAsync(flightId); + FlightAttendantCollectionResponseDocument document = await openApiClient.GetFlightCabinCrewMembersAsync(flightId); // Assert document.Data.Should().BeEmpty(); @@ -483,9 +483,9 @@ public async Task Getting_ToMany_relationship_translates_response() const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-personnel"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-personnel"", - ""first"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-personnel"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-crew-members"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-crew-members"", + ""first"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/cabin-crew-members"" }, ""data"": [{ ""type"": ""flight-attendants"", @@ -501,7 +501,7 @@ public async Task Getting_ToMany_relationship_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantIdentifierCollectionResponseDocument document = await openApiClient.GetFlightCabinPersonnelRelationshipAsync(flightId); + FlightAttendantIdentifierCollectionResponseDocument document = await openApiClient.GetFlightCabinCrewMembersRelationshipAsync(flightId); // Assert document.Data.Should().HaveCount(2); @@ -519,7 +519,7 @@ public async Task Posting_ToMany_relationship_produces_empty_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.PostFlightCabinPersonnelRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.PostFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List { @@ -548,7 +548,7 @@ public async Task Patching_ToMany_relationship_produces_empty_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.PatchFlightCabinPersonnelRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.PatchFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List { @@ -577,7 +577,7 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.DeleteFlightCabinPersonnelRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData + Func action = async () => await openApiClient.DeleteFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List { diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs index 2ed0d0fd1b..aa8d005095 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs @@ -30,7 +30,7 @@ public sealed class Flight : Identifiable public DateTime? ArrivesAt { get; set; } [HasMany] - public ISet CabinPersonnel { get; set; } + public ISet CabinCrewMembers { get; set; } [HasOne] public Airplane OperatingAirplane { get; set; } @@ -39,7 +39,7 @@ public sealed class Flight : Identifiable [NotMapped] public ICollection ServicesOnBoard { get; set; } - [HasMany] - public ICollection BackupCabinPersonnel { get; set; } + [HasOne] + public FlightAttendant Purser { get; set; } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs index a76d3153c5..0d6d74ca94 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs @@ -42,6 +42,6 @@ public sealed class FlightAttendant : Identifiable public ISet ScheduledForFlights { get; set; } [HasMany] - public ISet StandbyForFlights { get; set; } + public ISet PurserOnFlights { get; set; } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs index 1672698234..cf15a5a1ae 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/LegacyIntegrationDbContext.cs @@ -21,12 +21,12 @@ public LegacyIntegrationDbContext(DbContextOptions o protected override void OnModelCreating(ModelBuilder builder) { builder.Entity() - .HasMany(flight => flight.CabinPersonnel) + .HasMany(flight => flight.CabinCrewMembers) .WithMany(flightAttendant => flightAttendant.ScheduledForFlights); builder.Entity() - .HasMany(flight => flight.BackupCabinPersonnel) - .WithMany(flightAttendant => flightAttendant.StandbyForFlights); + .HasOne(flight => flight.Purser) + .WithMany(flightAttendant => flightAttendant.PurserOnFlights); } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index 79cc427c58..9626344a61 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -589,12 +589,12 @@ } } }, - "/api/v1/flight-attendants/{id}/scheduled-for-flights": { + "/api/v1/flight-attendants/{id}/purser-on-flights": { "get": { "tags": [ "flight-attendants" ], - "operationId": "get-flight-attendant-scheduled-for-flights", + "operationId": "get-flight-attendant-purser-on-flights", "parameters": [ { "name": "id", @@ -623,7 +623,7 @@ "tags": [ "flight-attendants" ], - "operationId": "head-flight-attendant-scheduled-for-flights", + "operationId": "head-flight-attendant-purser-on-flights", "parameters": [ { "name": "id", @@ -649,12 +649,12 @@ } } }, - "/api/v1/flight-attendants/{id}/relationships/scheduled-for-flights": { + "/api/v1/flight-attendants/{id}/relationships/purser-on-flights": { "get": { "tags": [ "flight-attendants" ], - "operationId": "get-flight-attendant-scheduled-for-flights-relationship", + "operationId": "get-flight-attendant-purser-on-flights-relationship", "parameters": [ { "name": "id", @@ -683,7 +683,7 @@ "tags": [ "flight-attendants" ], - "operationId": "head-flight-attendant-scheduled-for-flights-relationship", + "operationId": "head-flight-attendant-purser-on-flights-relationship", "parameters": [ { "name": "id", @@ -712,7 +712,7 @@ "tags": [ "flight-attendants" ], - "operationId": "post-flight-attendant-scheduled-for-flights-relationship", + "operationId": "post-flight-attendant-purser-on-flights-relationship", "parameters": [ { "name": "id", @@ -743,7 +743,7 @@ "tags": [ "flight-attendants" ], - "operationId": "patch-flight-attendant-scheduled-for-flights-relationship", + "operationId": "patch-flight-attendant-purser-on-flights-relationship", "parameters": [ { "name": "id", @@ -774,7 +774,7 @@ "tags": [ "flight-attendants" ], - "operationId": "delete-flight-attendant-scheduled-for-flights-relationship", + "operationId": "delete-flight-attendant-purser-on-flights-relationship", "parameters": [ { "name": "id", @@ -802,12 +802,12 @@ } } }, - "/api/v1/flight-attendants/{id}/standby-for-flights": { + "/api/v1/flight-attendants/{id}/scheduled-for-flights": { "get": { "tags": [ "flight-attendants" ], - "operationId": "get-flight-attendant-standby-for-flights", + "operationId": "get-flight-attendant-scheduled-for-flights", "parameters": [ { "name": "id", @@ -836,7 +836,7 @@ "tags": [ "flight-attendants" ], - "operationId": "head-flight-attendant-standby-for-flights", + "operationId": "head-flight-attendant-scheduled-for-flights", "parameters": [ { "name": "id", @@ -862,12 +862,12 @@ } } }, - "/api/v1/flight-attendants/{id}/relationships/standby-for-flights": { + "/api/v1/flight-attendants/{id}/relationships/scheduled-for-flights": { "get": { "tags": [ "flight-attendants" ], - "operationId": "get-flight-attendant-standby-for-flights-relationship", + "operationId": "get-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", @@ -896,7 +896,7 @@ "tags": [ "flight-attendants" ], - "operationId": "head-flight-attendant-standby-for-flights-relationship", + "operationId": "head-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", @@ -925,7 +925,7 @@ "tags": [ "flight-attendants" ], - "operationId": "post-flight-attendant-standby-for-flights-relationship", + "operationId": "post-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", @@ -956,7 +956,7 @@ "tags": [ "flight-attendants" ], - "operationId": "patch-flight-attendant-standby-for-flights-relationship", + "operationId": "patch-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", @@ -987,7 +987,7 @@ "tags": [ "flight-attendants" ], - "operationId": "delete-flight-attendant-standby-for-flights-relationship", + "operationId": "delete-flight-attendant-scheduled-for-flights-relationship", "parameters": [ { "name": "id", @@ -1202,12 +1202,12 @@ } } }, - "/api/v1/flights/{id}/backup-cabin-personnel": { + "/api/v1/flights/{id}/cabin-crew-members": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-backup-cabin-personnel", + "operationId": "get-flight-cabin-crew-members", "parameters": [ { "name": "id", @@ -1235,7 +1235,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-backup-cabin-personnel", + "operationId": "head-flight-cabin-crew-members", "parameters": [ { "name": "id", @@ -1260,12 +1260,12 @@ } } }, - "/api/v1/flights/{id}/relationships/backup-cabin-personnel": { + "/api/v1/flights/{id}/relationships/cabin-crew-members": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-backup-cabin-personnel-relationship", + "operationId": "get-flight-cabin-crew-members-relationship", "parameters": [ { "name": "id", @@ -1293,7 +1293,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-backup-cabin-personnel-relationship", + "operationId": "head-flight-cabin-crew-members-relationship", "parameters": [ { "name": "id", @@ -1321,7 +1321,7 @@ "tags": [ "flights" ], - "operationId": "post-flight-backup-cabin-personnel-relationship", + "operationId": "post-flight-cabin-crew-members-relationship", "parameters": [ { "name": "id", @@ -1351,7 +1351,7 @@ "tags": [ "flights" ], - "operationId": "patch-flight-backup-cabin-personnel-relationship", + "operationId": "patch-flight-cabin-crew-members-relationship", "parameters": [ { "name": "id", @@ -1381,7 +1381,7 @@ "tags": [ "flights" ], - "operationId": "delete-flight-backup-cabin-personnel-relationship", + "operationId": "delete-flight-cabin-crew-members-relationship", "parameters": [ { "name": "id", @@ -1408,12 +1408,12 @@ } } }, - "/api/v1/flights/{id}/cabin-personnel": { + "/api/v1/flights/{id}/operating-airplane": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-cabin-personnel", + "operationId": "get-flight-operating-airplane", "parameters": [ { "name": "id", @@ -1430,7 +1430,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-collection-response-document" + "$ref": "#/components/schemas/airplane-secondary-response-document" } } } @@ -1441,7 +1441,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-cabin-personnel", + "operationId": "head-flight-operating-airplane", "parameters": [ { "name": "id", @@ -1458,7 +1458,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-collection-response-document" + "$ref": "#/components/schemas/airplane-secondary-response-document" } } } @@ -1466,12 +1466,12 @@ } } }, - "/api/v1/flights/{id}/relationships/cabin-personnel": { + "/api/v1/flights/{id}/relationships/operating-airplane": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-cabin-personnel-relationship", + "operationId": "get-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1488,7 +1488,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" + "$ref": "#/components/schemas/airplane-identifier-response-document" } } } @@ -1499,7 +1499,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-cabin-personnel-relationship", + "operationId": "head-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1516,78 +1516,18 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/flight-attendant-identifier-collection-response-document" + "$ref": "#/components/schemas/airplane-identifier-response-document" } } } } } }, - "post": { - "tags": [ - "flights" - ], - "operationId": "post-flight-cabin-personnel-relationship", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" - } - } - } - }, - "responses": { - "204": { - "description": "Success" - } - } - }, "patch": { "tags": [ "flights" ], - "operationId": "patch-flight-cabin-personnel-relationship", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/vnd.api+json": { - "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" - } - } - } - }, - "responses": { - "204": { - "description": "Success" - } - } - }, - "delete": { - "tags": [ - "flights" - ], - "operationId": "delete-flight-cabin-personnel-relationship", + "operationId": "patch-flight-operating-airplane-relationship", "parameters": [ { "name": "id", @@ -1602,7 +1542,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + "$ref": "#/components/schemas/to-one-airplane-request-data" } } } @@ -1614,12 +1554,12 @@ } } }, - "/api/v1/flights/{id}/operating-airplane": { + "/api/v1/flights/{id}/purser": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-operating-airplane", + "operationId": "get-flight-purser", "parameters": [ { "name": "id", @@ -1636,7 +1576,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-secondary-response-document" + "$ref": "#/components/schemas/flight-attendant-secondary-response-document" } } } @@ -1647,7 +1587,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-operating-airplane", + "operationId": "head-flight-purser", "parameters": [ { "name": "id", @@ -1664,7 +1604,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-secondary-response-document" + "$ref": "#/components/schemas/flight-attendant-secondary-response-document" } } } @@ -1672,12 +1612,12 @@ } } }, - "/api/v1/flights/{id}/relationships/operating-airplane": { + "/api/v1/flights/{id}/relationships/purser": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-operating-airplane-relationship", + "operationId": "get-flight-purser-relationship", "parameters": [ { "name": "id", @@ -1694,7 +1634,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-identifier-response-document" + "$ref": "#/components/schemas/flight-attendant-identifier-response-document" } } } @@ -1705,7 +1645,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-operating-airplane-relationship", + "operationId": "head-flight-purser-relationship", "parameters": [ { "name": "id", @@ -1722,7 +1662,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-identifier-response-document" + "$ref": "#/components/schemas/flight-attendant-identifier-response-document" } } } @@ -1733,7 +1673,7 @@ "tags": [ "flights" ], - "operationId": "patch-flight-operating-airplane-relationship", + "operationId": "patch-flight-purser-relationship", "parameters": [ { "name": "id", @@ -1748,7 +1688,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-airplane-request-data" + "$ref": "#/components/schemas/to-one-flight-attendant-request-data" } } } @@ -2345,6 +2285,36 @@ }, "additionalProperties": false }, + "flight-attendant-identifier-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-document" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, "flight-attendant-patch-request-document": { "required": [ "data" @@ -2398,7 +2368,7 @@ "scheduled-for-flights": { "$ref": "#/components/schemas/to-many-flight-request-data" }, - "standby-for-flights": { + "purser-on-flights": { "$ref": "#/components/schemas/to-many-flight-request-data" } }, @@ -2410,7 +2380,7 @@ "scheduled-for-flights": { "$ref": "#/components/schemas/to-many-flight-request-data" }, - "standby-for-flights": { + "purser-on-flights": { "$ref": "#/components/schemas/to-many-flight-request-data" } }, @@ -2422,12 +2392,42 @@ "scheduled-for-flights": { "$ref": "#/components/schemas/to-many-flight-response-data" }, - "standby-for-flights": { + "purser-on-flights": { "$ref": "#/components/schemas/to-many-flight-response-data" } }, "additionalProperties": false }, + "flight-attendant-secondary-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-document" + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-attendant-data-in-response" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false + }, "flight-attendants-resource-type": { "enum": [ "flight-attendants" @@ -2672,14 +2672,14 @@ "flight-relationships-in-patch-request": { "type": "object", "properties": { - "cabin-personnel": { + "cabin-crew-members": { "$ref": "#/components/schemas/to-many-flight-attendant-request-data" }, "operating-airplane": { "$ref": "#/components/schemas/to-one-airplane-request-data" }, - "backup-cabin-personnel": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + "purser": { + "$ref": "#/components/schemas/to-one-flight-attendant-request-data" } }, "additionalProperties": false @@ -2687,14 +2687,14 @@ "flight-relationships-in-post-request": { "type": "object", "properties": { - "cabin-personnel": { + "cabin-crew-members": { "$ref": "#/components/schemas/to-many-flight-attendant-request-data" }, "operating-airplane": { "$ref": "#/components/schemas/to-one-airplane-request-data" }, - "backup-cabin-personnel": { - "$ref": "#/components/schemas/to-many-flight-attendant-request-data" + "purser": { + "$ref": "#/components/schemas/to-one-flight-attendant-request-data" } }, "additionalProperties": false @@ -2702,14 +2702,14 @@ "flight-relationships-in-response": { "type": "object", "properties": { - "cabin-personnel": { + "cabin-crew-members": { "$ref": "#/components/schemas/to-many-flight-attendant-response-data" }, "operating-airplane": { "$ref": "#/components/schemas/to-one-airplane-response-data" }, - "backup-cabin-personnel": { - "$ref": "#/components/schemas/to-many-flight-attendant-response-data" + "purser": { + "$ref": "#/components/schemas/to-one-flight-attendant-response-data" } }, "additionalProperties": false @@ -3008,6 +3008,51 @@ } }, "additionalProperties": false + }, + "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 + }, + "to-one-flight-attendant-response-data": { + "required": [ + "links" + ], + "type": "object", + "properties": { + "links": { + "$ref": "#/components/schemas/links-in-relationship-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} + }, + "data": { + "oneOf": [ + { + "$ref": "#/components/schemas/flight-attendant-identifier" + }, + { + "$ref": "#/components/schemas/null-value" + } + ] + } + }, + "additionalProperties": false } } } From 8ceff7b76129948ed4c8c496a848589065fbb398 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 12:21:20 +0200 Subject: [PATCH 31/35] fixed failed automatic rename 'openApiOpenApi' --- .../LegacyClient/RequestTests.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 43c645271a..0d53bd10a5 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -22,10 +22,10 @@ public async Task Getting_resource_collection_produces_expected_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCollectionAsync()); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightCollectionAsync()); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -41,10 +41,10 @@ public async Task Getting_resource_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -58,7 +58,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new FlightPostRequestDocument { @@ -73,7 +73,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ }; // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PostFlightAsync(requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PostFlightAsync(requestDocument)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -163,7 +163,7 @@ public async Task Partial_patching_resource_produces_expected_request() var lastServicedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new AirplanePatchRequestDocument { @@ -178,12 +178,12 @@ public async Task Partial_patching_resource_produces_expected_request() } }; - using (apiOpenApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.SerialNumber, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -215,10 +215,10 @@ public async Task Deleting_resource_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - await apiOpenApiClient.DeleteFlightAsync(flightId); + await openApiClient.DeleteFlightAsync(flightId); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Delete); @@ -233,10 +233,10 @@ public async Task Getting_secondary_resource_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightOperatingAirplaneAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightOperatingAirplaneAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -252,10 +252,10 @@ public async Task Getting_secondary_resources_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCabinCrewMembersAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightCabinCrewMembersAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -271,10 +271,10 @@ public async Task Getting_ToOne_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -290,7 +290,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToOneAirplaneRequestData { @@ -302,7 +302,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() }; // Act - await apiOpenApiClient.PatchFlightOperatingAirplaneRelationshipAsync(flightId, requestDocument); + await openApiClient.PatchFlightOperatingAirplaneRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); @@ -326,10 +326,10 @@ public async Task Getting_ToMany_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await apiOpenApiClient.GetFlightCabinCrewMembersRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightCabinCrewMembersRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -345,7 +345,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -365,7 +365,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() }; // Act - await apiOpenApiClient.PostFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); + await openApiClient.PostFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Post); @@ -395,7 +395,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -415,7 +415,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() }; // Act - await apiOpenApiClient.PatchFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); + await openApiClient.PatchFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); @@ -445,7 +445,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient apiOpenApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -465,7 +465,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() }; // Act - await apiOpenApiClient.DeleteFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); + await openApiClient.DeleteFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Delete); From 5a4a06f7d7b0665eb0aaf2276fa4114fb50c6d3a Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 17:51:29 +0200 Subject: [PATCH 32/35] Flight: replaced OperatingAirplane with Purser, added to many relationship to new model Passenger --- .../LegacyClient/RequestTests.cs | 24 +- .../LegacyClient/ResponseTests.cs | 98 ++--- .../LegacyOpenApiIntegration/CabinArea.cs | 12 + .../LegacyOpenApiIntegration/Flight.cs | 6 +- .../FlightAttendant.cs | 9 +- .../FlightAttendantsController.cs | 4 +- .../LegacyOpenApiIntegration/Passenger.cs | 22 + .../PassengersController.cs | 15 + .../LegacyOpenApiIntegration/swagger.json | 391 +++++++++++------- 9 files changed, 348 insertions(+), 233 deletions(-) create mode 100644 test/OpenApiTests/LegacyOpenApiIntegration/CabinArea.cs create mode 100644 test/OpenApiTests/LegacyOpenApiIntegration/Passenger.cs create mode 100644 test/OpenApiTests/LegacyOpenApiIntegration/PassengersController.cs diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 0d53bd10a5..4f31410fe9 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -67,7 +67,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ Type = FlightsResourceType.Flights, Relationships = new FlightRelationshipsInPostRequest { - OperatingAirplane = new ToOneAirplaneRequestData() + Purser = new ToOneFlightAttendantRequestData() } } }; @@ -87,7 +87,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ ""data"": { ""type"": ""flights"", ""relationships"": { - ""operating-airplane"": { + ""purser"": { ""data"": null } } @@ -236,12 +236,12 @@ public async Task Getting_secondary_resource_produces_expected_request() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightOperatingAirplaneAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightPurserAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/operating-airplane"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/purser"); wrapper.RequestBody.Should().BeNull(); } @@ -274,12 +274,12 @@ public async Task Getting_ToOne_relationship_produces_expected_request() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightPurserRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); wrapper.Request.Method.Should().Be(HttpMethod.Get); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/operating-airplane"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/purser"); wrapper.RequestBody.Should().BeNull(); } @@ -292,28 +292,28 @@ public async Task Patching_ToOne_relationship_produces_expected_request() using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); - var requestDocument = new ToOneAirplaneRequestData + var requestDocument = new ToOneFlightAttendantRequestData { - Data = new AirplaneIdentifier + Data = new FlightAttendantIdentifier { Id = "bBJHu", - Type = AirplanesResourceType.Airplanes + Type = FlightAttendantsResourceType.FlightAttendants } }; // Act - await openApiClient.PatchFlightOperatingAirplaneRelationshipAsync(flightId, requestDocument); + await openApiClient.PatchFlightPurserRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); - wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/operating-airplane"); + wrapper.Request.RequestUri.Should().Be(HostPrefix + $"flights/{flightId}/relationships/purser"); wrapper.Request.Content.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType.Should().NotBeNull(); wrapper.Request.Content!.Headers.ContentType!.ToString().Should().Be(HeaderConstants.MediaType); wrapper.RequestBody.Should().BeJson(@"{ ""data"": { - ""type"": ""airplanes"", + ""type"": ""flight-attendants"", ""id"": ""bBJHu"" } }"); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 6dc6d3e4c9..910356dd8b 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -27,9 +27,9 @@ public async Task Getting_resource_collection_translates_response() const string flightDepartsAt = "2014-11-25T00:00:00"; const string documentMetaValue = "1"; const string flightMetaValue = "https://api.jsonapi.net/docs/#get-flights"; - const string operatingAirplaneMetaValue = "https://jsonapi.net/api/docs/#get-flight-operating-airplane"; - const string cabinPersonnelMetaValue = "https://jsonapi.net/api/docs/#get-flight-cabin-crew-members"; const string purserMetaValue = "https://jsonapi.net/api/docs/#get-flight-purser"; + const string cabinPersonnelMetaValue = "https://jsonapi.net/api/docs/#get-flight-cabin-crew-members"; + const string passengersMetaValue = "https://jsonapi.net/api/docs/#get-flight-passengers"; const string topLevelLink = HostPrefix + "flights"; const string flightResourceLink = topLevelLink + "/" + flightId; @@ -59,13 +59,13 @@ public async Task Getting_resource_collection_translates_response() ] }, ""relationships"": { - ""operating-airplane"": { + ""purser"": { ""links"": { - ""self"": """ + flightResourceLink + @"/relationships/operating-airplane"", - ""related"": """ + flightResourceLink + @"/operating-airplane"" + ""self"": """ + flightResourceLink + @"/relationships/purser"", + ""related"": """ + flightResourceLink + @"/purser"" }, ""meta"": { - ""docs"": """ + operatingAirplaneMetaValue + @""" + ""docs"": """ + purserMetaValue + @""" } }, ""cabin-crew-members"": { @@ -77,13 +77,13 @@ public async Task Getting_resource_collection_translates_response() ""docs"": """ + cabinPersonnelMetaValue + @""" } }, - ""purser"": { + ""passengers"": { ""links"": { - ""self"": """ + flightResourceLink + @"/relationships/purser"", - ""related"": """ + flightResourceLink + @"/purser"" + ""self"": """ + flightResourceLink + @"/relationships/passengers"", + ""related"": """ + flightResourceLink + @"/passengers"" }, ""meta"": { - ""docs"": """ + purserMetaValue + @""" + ""docs"": """ + passengersMetaValue + @""" } } }, @@ -128,11 +128,11 @@ public async Task Getting_resource_collection_translates_response() flight.Attributes.DepartsAt.Should().Be(DateTimeOffset.Parse(flightDepartsAt, new CultureInfo("en-GB"))); flight.Attributes.ArrivesAt.Should().Be(null); - flight.Relationships.OperatingAirplane.Data.Should().BeNull(); - flight.Relationships.OperatingAirplane.Links.Self.Should().Be(flightResourceLink + "/relationships/operating-airplane"); - flight.Relationships.OperatingAirplane.Links.Related.Should().Be(flightResourceLink + "/operating-airplane"); - flight.Relationships.OperatingAirplane.Meta.Should().HaveCount(1); - flight.Relationships.OperatingAirplane.Meta["docs"].Should().Be(operatingAirplaneMetaValue); + flight.Relationships.Purser.Data.Should().BeNull(); + flight.Relationships.Purser.Links.Self.Should().Be(flightResourceLink + "/relationships/purser"); + flight.Relationships.Purser.Links.Related.Should().Be(flightResourceLink + "/purser"); + flight.Relationships.Purser.Meta.Should().HaveCount(1); + flight.Relationships.Purser.Meta["docs"].Should().Be(purserMetaValue); flight.Relationships.CabinCrewMembers.Data.Should().BeNull(); flight.Relationships.CabinCrewMembers.Links.Self.Should().Be(flightResourceLink + "/relationships/cabin-crew-members"); @@ -140,11 +140,11 @@ public async Task Getting_resource_collection_translates_response() flight.Relationships.CabinCrewMembers.Meta.Should().HaveCount(1); flight.Relationships.CabinCrewMembers.Meta["docs"].Should().Be(cabinPersonnelMetaValue); - flight.Relationships.Purser.Data.Should().BeNull(); - flight.Relationships.Purser.Links.Self.Should().Be(flightResourceLink + "/relationships/purser"); - flight.Relationships.Purser.Links.Related.Should().Be(flightResourceLink + "/purser"); - flight.Relationships.Purser.Meta.Should().HaveCount(1); - flight.Relationships.Purser.Meta["docs"].Should().Be(purserMetaValue); + flight.Relationships.Passengers.Data.Should().BeNull(); + flight.Relationships.Passengers.Links.Self.Should().Be(flightResourceLink + "/relationships/passengers"); + flight.Relationships.Passengers.Links.Related.Should().Be(flightResourceLink + "/passengers"); + flight.Relationships.Passengers.Meta.Should().HaveCount(1); + flight.Relationships.Passengers.Meta["docs"].Should().Be(passengersMetaValue); } [Fact] @@ -230,16 +230,16 @@ public async Task Posting_resource_translates_response() const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-crew-members,purser"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=purser,cabin-crew-members,passengers"" }, ""data"": { ""type"": ""flights"", ""id"": """ + flightId + @""", ""relationships"": { - ""operating-airplane"": { + ""purser"": { ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/operating-airplane"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/operating-airplane"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/purser"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/purser"" }, ""data"": null }, @@ -255,16 +255,16 @@ public async Task Posting_resource_translates_response() } ], }, - ""purser"": { + ""passengers"": { ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/purser"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/purser"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/passengers"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/passengers"" }, - ""data"": null + ""data"": [ ] } }, ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-crew-members,purser"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=purser,cabin-crew-members,passengers"" } } }"; @@ -280,12 +280,12 @@ public async Task Posting_resource_translates_response() Type = FlightsResourceType.Flights, Relationships = new FlightRelationshipsInPostRequest { - OperatingAirplane = new ToOneAirplaneRequestData + Purser = new ToOneFlightAttendantRequestData { - Data = new AirplaneIdentifier + Data = new FlightAttendantIdentifier { Id = "XxuIu", - Type = AirplanesResourceType.Airplanes + Type = FlightAttendantsResourceType.FlightAttendants } } } @@ -294,11 +294,11 @@ public async Task Posting_resource_translates_response() // Assert document.Data.Attributes.Should().BeNull(); - document.Data.Relationships.OperatingAirplane.Data.Should().BeNull(); + document.Data.Relationships.Purser.Data.Should().BeNull(); document.Data.Relationships.CabinCrewMembers.Data.Should().HaveCount(1); document.Data.Relationships.CabinCrewMembers.Data.First().Id.Should().Be(flightAttendantId); document.Data.Relationships.CabinCrewMembers.Data.First().Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); - document.Data.Relationships.Purser.Data.Should().BeNull(); + document.Data.Relationships.Passengers.Data.Should().BeEmpty(); } [Fact] @@ -315,7 +315,7 @@ public async Task Patching_resource_with_side_effects_translates_response() ""type"": ""flights"", ""id"": """ + flightId + @""", ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=operating-airplane,cabin-crew-members,purser"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"&fields[flights]&include=purser,cabin-crew-members,passengers"" } } }"; @@ -384,9 +384,9 @@ public async Task Getting_secondary_resource_translates_response() const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/operating-airplane"", - ""first"": """ + HostPrefix + @"flights/" + flightId + @"/operating-airplane"", - ""last"": """ + HostPrefix + @"flights/" + flightId + @"/operating-airplane"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/purser"", + ""first"": """ + HostPrefix + @"flights/" + flightId + @"/purser"", + ""last"": """ + HostPrefix + @"flights/" + flightId + @"/purser"" }, ""data"": null }"; @@ -395,7 +395,7 @@ public async Task Getting_secondary_resource_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - AirplaneSecondaryResponseDocument document = await openApiClient.GetFlightOperatingAirplaneAsync(flightId); + FlightAttendantSecondaryResponseDocument document = await openApiClient.GetFlightPurserAsync(flightId); // Assert document.Data.Should().BeNull(); @@ -430,16 +430,16 @@ public async Task Getting_ToOne_relationship_translates_response() { // Arrange const string flightId = "ZvuH1"; - const string operatingAirplaneId = "bBJHu"; + const string purserId = "bBJHu"; const string responseBody = @"{ ""links"": { - ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/operating-airplane"", - ""related"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/operating-airplane"" + ""self"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/purser"", + ""related"": """ + HostPrefix + @"flights/" + flightId + @"/relationships/purser"" }, ""data"": { - ""type"": ""airplanes"", - ""id"": """ + operatingAirplaneId + @""" + ""type"": ""flight-attendants"", + ""id"": """ + purserId + @""" } }"; @@ -447,11 +447,11 @@ public async Task Getting_ToOne_relationship_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - AirplaneIdentifierResponseDocument document = await openApiClient.GetFlightOperatingAirplaneRelationshipAsync(flightId); + FlightAttendantIdentifierResponseDocument document = await openApiClient.GetFlightPurserRelationshipAsync(flightId); // Assert document.Data.Should().NotBeNull(); - document.Data.Id.Should().Be(operatingAirplaneId); + document.Data.Id.Should().Be(purserId); document.Data.Type.Should().Be(FlightAttendantsResourceType.FlightAttendants); } @@ -463,12 +463,12 @@ public async Task Patching_ToOne_relationship_translates_response() IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); // Act - await openApiClient.PatchFlightOperatingAirplaneRelationshipAsync("ZvuH1", new ToOneAirplaneRequestData + await openApiClient.PatchFlightPurserRelationshipAsync("ZvuH1", new ToOneFlightAttendantRequestData { - Data = new AirplaneIdentifier + Data = new FlightAttendantIdentifier { Id = "Adk2a", - Type = AirplanesResourceType.Airplanes + Type = FlightAttendantsResourceType.FlightAttendants } }); } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/CabinArea.cs b/test/OpenApiTests/LegacyOpenApiIntegration/CabinArea.cs new file mode 100644 index 0000000000..fdee6cd686 --- /dev/null +++ b/test/OpenApiTests/LegacyOpenApiIntegration/CabinArea.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; + +namespace OpenApiTests.LegacyOpenApiIntegration +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public enum CabinArea + { + FirstClass, + BusinessClass, + EconomyClass + } +} diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs index aa8d005095..f8dca69478 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Flight.cs @@ -33,13 +33,13 @@ public sealed class Flight : Identifiable public ISet CabinCrewMembers { get; set; } [HasOne] - public Airplane OperatingAirplane { get; set; } + public FlightAttendant Purser { get; set; } [Attr] [NotMapped] public ICollection ServicesOnBoard { get; set; } - [HasOne] - public FlightAttendant Purser { get; set; } + [HasMany] + public ICollection Passengers { get; set; } } } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs index 0d6d74ca94..520dcd8ffc 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendant.cs @@ -7,19 +7,14 @@ namespace OpenApiTests.LegacyOpenApiIntegration { [UsedImplicitly(ImplicitUseTargetFlags.Members)] - public sealed class FlightAttendant : Identifiable + public sealed class FlightAttendant : Identifiable { [Attr(Capabilities = AttrCapabilities.AllowView | AttrCapabilities.AllowFilter)] - public override long Id { get; set; } + public override string Id { get; set; } [Attr(Capabilities = AttrCapabilities.None)] public FlightAttendantExpertiseLevel ExpertiseLevel { get; set; } - [Attr(PublicName = "document-number", Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] - [Required] - [MaxLength(9)] - public string PassportNumber { get; set; } - [Attr(Capabilities = AttrCapabilities.All)] [Required] [EmailAddress] diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs index 2eae98caba..43adc8d0eb 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs +++ b/test/OpenApiTests/LegacyOpenApiIntegration/FlightAttendantsController.cs @@ -5,9 +5,9 @@ namespace OpenApiTests.LegacyOpenApiIntegration { - public sealed class FlightAttendantsController : JsonApiController + public sealed class FlightAttendantsController : JsonApiController { - public FlightAttendantsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + public FlightAttendantsController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) : base(options, loggerFactory, resourceService) { } diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/Passenger.cs b/test/OpenApiTests/LegacyOpenApiIntegration/Passenger.cs new file mode 100644 index 0000000000..263393bac5 --- /dev/null +++ b/test/OpenApiTests/LegacyOpenApiIntegration/Passenger.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; +using JetBrains.Annotations; +using JsonApiDotNetCore.Resources; +using JsonApiDotNetCore.Resources.Annotations; + +namespace OpenApiTests.LegacyOpenApiIntegration +{ + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + public sealed class Passenger : Identifiable + { + [Attr(PublicName = "document-number", Capabilities = AttrCapabilities.AllowCreate | AttrCapabilities.AllowChange)] + [Required] + [MaxLength(9)] + public string PassportNumber { get; set; } + + [Attr] + public string FullName { get; set; } + + [Attr] + public CabinArea CabinArea { get; set; } + } +} diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/PassengersController.cs b/test/OpenApiTests/LegacyOpenApiIntegration/PassengersController.cs new file mode 100644 index 0000000000..ea056c4474 --- /dev/null +++ b/test/OpenApiTests/LegacyOpenApiIntegration/PassengersController.cs @@ -0,0 +1,15 @@ +using JsonApiDotNetCore.Configuration; +using JsonApiDotNetCore.Controllers; +using JsonApiDotNetCore.Services; +using Microsoft.Extensions.Logging; + +namespace OpenApiTests.LegacyOpenApiIntegration +{ + public sealed class PassengersController : JsonApiController + { + public PassengersController(IJsonApiOptions options, ILoggerFactory loggerFactory, IResourceService resourceService) + : base(options, loggerFactory, resourceService) + { + } + } +} diff --git a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json index 9626344a61..3ed426bf24 100644 --- a/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json +++ b/test/OpenApiTests/LegacyOpenApiIntegration/swagger.json @@ -478,8 +478,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -507,8 +506,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -536,8 +534,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -577,8 +574,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -601,8 +597,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -630,8 +625,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -661,8 +655,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -690,8 +683,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -719,8 +711,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -750,8 +741,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -781,8 +771,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -814,8 +803,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -843,8 +831,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -874,8 +861,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -903,8 +889,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -932,8 +917,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -963,8 +947,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -994,8 +977,7 @@ "in": "path", "required": true, "schema": { - "type": "integer", - "format": "int64" + "type": "string" } } ], @@ -1408,12 +1390,12 @@ } } }, - "/api/v1/flights/{id}/operating-airplane": { + "/api/v1/flights/{id}/passengers": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-operating-airplane", + "operationId": "get-flight-passengers", "parameters": [ { "name": "id", @@ -1430,7 +1412,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-secondary-response-document" + "$ref": "#/components/schemas/passenger-collection-response-document" } } } @@ -1441,7 +1423,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-operating-airplane", + "operationId": "head-flight-passengers", "parameters": [ { "name": "id", @@ -1458,7 +1440,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-secondary-response-document" + "$ref": "#/components/schemas/passenger-collection-response-document" } } } @@ -1466,12 +1448,12 @@ } } }, - "/api/v1/flights/{id}/relationships/operating-airplane": { + "/api/v1/flights/{id}/relationships/passengers": { "get": { "tags": [ "flights" ], - "operationId": "get-flight-operating-airplane-relationship", + "operationId": "get-flight-passengers-relationship", "parameters": [ { "name": "id", @@ -1488,7 +1470,7 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-identifier-response-document" + "$ref": "#/components/schemas/passenger-identifier-collection-response-document" } } } @@ -1499,7 +1481,7 @@ "tags": [ "flights" ], - "operationId": "head-flight-operating-airplane-relationship", + "operationId": "head-flight-passengers-relationship", "parameters": [ { "name": "id", @@ -1516,18 +1498,48 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/airplane-identifier-response-document" + "$ref": "#/components/schemas/passenger-identifier-collection-response-document" } } } } } }, + "post": { + "tags": [ + "flights" + ], + "operationId": "post-flight-passengers-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-passenger-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, "patch": { "tags": [ "flights" ], - "operationId": "patch-flight-operating-airplane-relationship", + "operationId": "patch-flight-passengers-relationship", "parameters": [ { "name": "id", @@ -1542,7 +1554,37 @@ "content": { "application/vnd.api+json": { "schema": { - "$ref": "#/components/schemas/to-one-airplane-request-data" + "$ref": "#/components/schemas/to-many-passenger-request-data" + } + } + } + }, + "responses": { + "204": { + "description": "Success" + } + } + }, + "delete": { + "tags": [ + "flights" + ], + "operationId": "delete-flight-passengers-relationship", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/vnd.api+json": { + "schema": { + "$ref": "#/components/schemas/to-many-passenger-request-data" } } } @@ -1917,52 +1959,6 @@ }, "additionalProperties": false }, - "airplane-identifier": { - "required": [ - "id", - "type" - ], - "type": "object", - "properties": { - "type": { - "$ref": "#/components/schemas/airplanes-resource-type" - }, - "id": { - "type": "string" - } - }, - "additionalProperties": false - }, - "airplane-identifier-response-document": { - "required": [ - "data", - "links" - ], - "type": "object", - "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-identifier-document" - }, - "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/airplane-identifier" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] - } - }, - "additionalProperties": false - }, "airplane-patch-request-document": { "required": [ "data" @@ -2037,49 +2033,23 @@ }, "additionalProperties": false }, - "airplane-secondary-response-document": { - "required": [ - "data", - "links" - ], - "type": "object", - "properties": { - "meta": { - "type": "object", - "additionalProperties": {} - }, - "jsonapi": { - "$ref": "#/components/schemas/jsonapi-object" - }, - "links": { - "$ref": "#/components/schemas/links-in-resource-document" - }, - "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/airplane-data-in-response" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] - } - }, - "additionalProperties": false - }, "airplanes-resource-type": { "enum": [ "airplanes" ], "type": "string" }, + "cabin-area": { + "enum": [ + "FirstClass", + "BusinessClass", + "EconomyClass" + ], + "type": "string" + }, "flight-attendant-attributes-in-patch-request": { "type": "object", "properties": { - "document-number": { - "maxLength": 9, - "type": "string" - }, "email-address": { "type": "string", "format": "email" @@ -2095,17 +2065,12 @@ }, "flight-attendant-attributes-in-post-request": { "required": [ - "document-number", "email-address", "age", "profile-image-url" ], "type": "object", "properties": { - "document-number": { - "maxLength": 9, - "type": "string" - }, "email-address": { "type": "string", "format": "email" @@ -2675,11 +2640,11 @@ "cabin-crew-members": { "$ref": "#/components/schemas/to-many-flight-attendant-request-data" }, - "operating-airplane": { - "$ref": "#/components/schemas/to-one-airplane-request-data" - }, "purser": { "$ref": "#/components/schemas/to-one-flight-attendant-request-data" + }, + "passengers": { + "$ref": "#/components/schemas/to-many-passenger-request-data" } }, "additionalProperties": false @@ -2690,11 +2655,11 @@ "cabin-crew-members": { "$ref": "#/components/schemas/to-many-flight-attendant-request-data" }, - "operating-airplane": { - "$ref": "#/components/schemas/to-one-airplane-request-data" - }, "purser": { "$ref": "#/components/schemas/to-one-flight-attendant-request-data" + }, + "passengers": { + "$ref": "#/components/schemas/to-many-passenger-request-data" } }, "additionalProperties": false @@ -2705,11 +2670,11 @@ "cabin-crew-members": { "$ref": "#/components/schemas/to-many-flight-attendant-response-data" }, - "operating-airplane": { - "$ref": "#/components/schemas/to-one-airplane-response-data" - }, "purser": { "$ref": "#/components/schemas/to-one-flight-attendant-response-data" + }, + "passengers": { + "$ref": "#/components/schemas/to-many-passenger-response-data" } }, "additionalProperties": false @@ -2890,6 +2855,120 @@ }, "nullable": true }, + "passenger-attributes-in-response": { + "type": "object", + "properties": { + "full-name": { + "type": "string", + "nullable": true + }, + "cabin-area": { + "$ref": "#/components/schemas/cabin-area" + } + }, + "additionalProperties": false + }, + "passenger-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-collection-document" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-data-in-response" + } + } + }, + "additionalProperties": false + }, + "passenger-data-in-response": { + "required": [ + "id", + "links", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/passengers-resource-type" + }, + "id": { + "type": "string" + }, + "attributes": { + "$ref": "#/components/schemas/passenger-attributes-in-response" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-object" + }, + "meta": { + "type": "object", + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "passenger-identifier": { + "required": [ + "id", + "type" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/passengers-resource-type" + }, + "id": { + "type": "string" + } + }, + "additionalProperties": false + }, + "passenger-identifier-collection-response-document": { + "required": [ + "data", + "links" + ], + "type": "object", + "properties": { + "meta": { + "type": "object", + "additionalProperties": {} + }, + "jsonapi": { + "$ref": "#/components/schemas/jsonapi-object" + }, + "links": { + "$ref": "#/components/schemas/links-in-resource-identifier-collection-document" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-identifier" + } + } + }, + "additionalProperties": false + }, + "passengers-resource-type": { + "enum": [ + "passengers" + ], + "type": "string" + }, "to-many-flight-attendant-request-data": { "required": [ "data" @@ -2964,26 +3043,22 @@ }, "additionalProperties": false }, - "to-one-airplane-request-data": { + "to-many-passenger-request-data": { "required": [ "data" ], "type": "object", "properties": { "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/airplane-identifier" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-identifier" + } } }, "additionalProperties": false }, - "to-one-airplane-response-data": { + "to-many-passenger-response-data": { "required": [ "links" ], @@ -2997,14 +3072,10 @@ "additionalProperties": {} }, "data": { - "oneOf": [ - { - "$ref": "#/components/schemas/airplane-identifier" - }, - { - "$ref": "#/components/schemas/null-value" - } - ] + "type": "array", + "items": { + "$ref": "#/components/schemas/passenger-identifier" + } } }, "additionalProperties": false From 7a88a64298f4860fe4b8490aa21b77b912f1d211 Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 20:47:24 +0200 Subject: [PATCH 33/35] Review feedback --- .../JsonApiObjectNullabilityProcessor.cs | 3 +-- ...lientAttributeRegistrationLifeTimeTests.cs | 6 ++--- .../LegacyClient/ResponseTests.cs | 23 ++++++++++--------- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiObjectNullabilityProcessor.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiObjectNullabilityProcessor.cs index be6f4c9283..29d1d71bd6 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiObjectNullabilityProcessor.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiObjectNullabilityProcessor.cs @@ -7,8 +7,7 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents /// /// /// Initially these entries are marked nullable by Swashbuckle because nullable reference types are not enabled. This post-processing step can be removed - /// entirely once we enable nullable reference types. See eg - /// https://github.com/degreed/JsonApiCorePrototype/blob/936db8950d925f1b8a055cf5d8bba753f6579094/src/Web/OpenApi/JsonApiObjects/Documents/ManyResourceIdentifierResponseDocument.cs#L7 + /// entirely once we enable nullable reference types. /// internal sealed class JsonApiObjectNullabilityProcessor { diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs index 3d167507b3..0a8c80fd71 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs @@ -5,8 +5,6 @@ using TestBuildingBlocks; using Xunit; -#pragma warning disable AV1704 // Don't include numbers in variables, parameters and type members - namespace OpenApiClientTests.LegacyClient { public sealed class ClientAttributeRegistrationLifetimeTests @@ -135,7 +133,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ airplane => airplane.AirtimeInHours)) { using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, - airplane => airplane.IsInMaintenance)) + airplane => airplane.SerialNumber)) { } @@ -149,7 +147,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ ""type"": ""airplanes"", ""id"": """ + airplaneId2 + @""", ""attributes"": { - ""is-in-maintenance"": false + ""serial-number"": null } } }"); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 910356dd8b..6ede8a6df2 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -9,8 +9,6 @@ using OpenApiClientTests.LegacyClient.GeneratedCode; using Xunit; -#pragma warning disable AV1704 // Don't include numbers in variables, parameters and type members - namespace OpenApiClientTests.LegacyClient { public sealed class ResponseTests @@ -22,13 +20,13 @@ public async Task Getting_resource_collection_translates_response() { // Arrange const string flightId = "ZvuH1"; - const string flightDestination = "Destination of Flight"; - const string fightServiceOnBoard = "Movies"; + const string flightDestination = "Amsterdam"; + const string flightServiceOnBoard = "Movies"; const string flightDepartsAt = "2014-11-25T00:00:00"; const string documentMetaValue = "1"; - const string flightMetaValue = "https://api.jsonapi.net/docs/#get-flights"; + const string flightMetaValue = "https://jsonapi.net/api/docs/#get-flights"; const string purserMetaValue = "https://jsonapi.net/api/docs/#get-flight-purser"; - const string cabinPersonnelMetaValue = "https://jsonapi.net/api/docs/#get-flight-cabin-crew-members"; + const string cabinCrewMembersMetaValue = "https://jsonapi.net/api/docs/#get-flight-cabin-crew-members"; const string passengersMetaValue = "https://jsonapi.net/api/docs/#get-flight-passengers"; const string topLevelLink = HostPrefix + "flights"; const string flightResourceLink = topLevelLink + "/" + flightId; @@ -53,7 +51,7 @@ public async Task Getting_resource_collection_translates_response() ""departs-at"": """ + flightDepartsAt + @""", ""arrives-at"": null, ""services-on-board"": [ - """ + fightServiceOnBoard + @""", + """ + flightServiceOnBoard + @""", """", null ] @@ -74,7 +72,7 @@ public async Task Getting_resource_collection_translates_response() ""related"": """ + flightResourceLink + @"/cabin-crew-members"" }, ""meta"": { - ""docs"": """ + cabinPersonnelMetaValue + @""" + ""docs"": """ + cabinCrewMembersMetaValue + @""" } }, ""passengers"": { @@ -120,13 +118,14 @@ public async Task Getting_resource_collection_translates_response() flight.Meta["docs"].Should().Be(flightMetaValue); flight.Attributes.FinalDestination.Should().Be(flightDestination); + flight.Attributes.StopOverDestination.Should().Be(null); flight.Attributes.ServicesOnBoard.Should().HaveCount(3); - flight.Attributes.ServicesOnBoard.ElementAt(0).Should().Be(fightServiceOnBoard); + flight.Attributes.ServicesOnBoard.ElementAt(0).Should().Be(flightServiceOnBoard); flight.Attributes.ServicesOnBoard.ElementAt(1).Should().Be(string.Empty); flight.Attributes.ServicesOnBoard.ElementAt(2).Should().BeNull(); flight.Attributes.OperatedBy.Should().Be(Airline.DeltaAirLines); flight.Attributes.DepartsAt.Should().Be(DateTimeOffset.Parse(flightDepartsAt, new CultureInfo("en-GB"))); - flight.Attributes.ArrivesAt.Should().Be(null); + flight.Attributes.ArrivesAt.Should().BeNull(); flight.Relationships.Purser.Data.Should().BeNull(); flight.Relationships.Purser.Links.Self.Should().Be(flightResourceLink + "/relationships/purser"); @@ -138,7 +137,7 @@ public async Task Getting_resource_collection_translates_response() flight.Relationships.CabinCrewMembers.Links.Self.Should().Be(flightResourceLink + "/relationships/cabin-crew-members"); flight.Relationships.CabinCrewMembers.Links.Related.Should().Be(flightResourceLink + "/cabin-crew-members"); flight.Relationships.CabinCrewMembers.Meta.Should().HaveCount(1); - flight.Relationships.CabinCrewMembers.Meta["docs"].Should().Be(cabinPersonnelMetaValue); + flight.Relationships.CabinCrewMembers.Meta["docs"].Should().Be(cabinCrewMembersMetaValue); flight.Relationships.Passengers.Data.Should().BeNull(); flight.Relationships.Passengers.Links.Self.Should().Be(flightResourceLink + "/relationships/passengers"); @@ -187,6 +186,8 @@ public async Task Getting_resource_translates_response() document.Data.Attributes.ArrivesAt.Should().Be(DateTimeOffset.Parse(arrivesAtWithUtcOffset)); document.Data.Attributes.ServicesOnBoard.Should().BeNull(); document.Data.Attributes.FinalDestination.Should().BeNull(); + document.Data.Attributes.StopOverDestination.Should().BeNull(); + document.Data.Attributes.OperatedBy.Should().Be(default(Airline)); } From 4073c3f1f7b759934b49fd385f04e2366eef004c Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 20:54:23 +0200 Subject: [PATCH 34/35] Fix client lifetime test --- .../LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs index 0a8c80fd71..16ea0fe9fd 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs @@ -147,7 +147,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ ""type"": ""airplanes"", ""id"": """ + airplaneId2 + @""", ""attributes"": { - ""serial-number"": null + ""is-in-maintenance"": false } } }"); From d3ebfaf911f5b8e706eb351707ccffc284dd6e1f Mon Sep 17 00:00:00 2001 From: maurei Date: Fri, 17 Sep 2021 21:53:54 +0200 Subject: [PATCH 35/35] Feedback --- ...lientAttributeRegistrationLifeTimeTests.cs | 66 ++++++++--------- .../LegacyClient/RequestTests.cs | 60 ++++++++-------- .../LegacyClient/ResponseTests.cs | 71 +++++++++---------- 3 files changed, 98 insertions(+), 99 deletions(-) diff --git a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs index 16ea0fe9fd..f9e48c5da0 100644 --- a/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ClientAttributeRegistrationLifeTimeTests.cs @@ -14,7 +14,7 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -28,16 +28,16 @@ public async Task Disposed_attribute_registration_for_document_does_not_affect_r } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } wrapper.ChangeResponse(HttpStatusCode.NoContent, null); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); // Assert wrapper.RequestBody.Should().BeJson(@"{ @@ -56,7 +56,7 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -73,17 +73,17 @@ public async Task Attribute_registration_can_be_used_for_multiple_requests() } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); wrapper.ChangeResponse(HttpStatusCode.NoContent, null); requestDocument.Data.Attributes.AirtimeInHours = null; // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -103,7 +103,7 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -129,16 +129,16 @@ public async Task Request_is_unaffected_by_attribute_registration_for_different_ } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { } // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } // Assert @@ -158,7 +158,7 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId = "XUuiP"; @@ -175,13 +175,13 @@ public async Task Attribute_values_can_be_changed_after_attribute_registration() } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.IsInMaintenance)) { requestDocument.Data.Attributes.IsInMaintenance = false; // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -201,7 +201,7 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -224,14 +224,14 @@ public async Task Attribute_registration_is_unaffected_by_successive_attribute_r } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.IsInMaintenance)) { - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); } } @@ -252,7 +252,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -266,10 +266,10 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId1, requestDocument1)); } const string airplaneId2 = "DJy1u"; @@ -289,11 +289,11 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } // Assert @@ -314,7 +314,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument1 = new AirplanePostRequestDocument { @@ -325,10 +325,10 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.AirtimeInHours)) { - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PostAirplaneAsync(requestDocument1)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument1)); } const string airplaneId = "DJy1u"; @@ -348,11 +348,11 @@ public async Task Attribute_registration_is_unaffected_by_preceding_disposed_att wrapper.ChangeResponse(HttpStatusCode.NoContent, null); - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument2)); } // Assert @@ -373,7 +373,7 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); const string airplaneId1 = "XUuiP"; @@ -399,14 +399,14 @@ public async Task Attribute_registration_is_unaffected_by_preceding_attribute_re } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument1, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument1, airplane => airplane.SerialNumber)) { - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument2, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument2, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId2, requestDocument2)); } } diff --git a/test/OpenApiClientTests/LegacyClient/RequestTests.cs b/test/OpenApiClientTests/LegacyClient/RequestTests.cs index 4f31410fe9..ff9f1bbe56 100644 --- a/test/OpenApiClientTests/LegacyClient/RequestTests.cs +++ b/test/OpenApiClientTests/LegacyClient/RequestTests.cs @@ -22,10 +22,10 @@ public async Task Getting_resource_collection_produces_expected_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightCollectionAsync()); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCollectionAsync()); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -41,10 +41,10 @@ public async Task Getting_resource_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -58,7 +58,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new FlightPostRequestDocument { @@ -73,7 +73,7 @@ public async Task Partial_posting_resource_with_selected_relationships_produces_ }; // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PostFlightAsync(requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostFlightAsync(requestDocument)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -100,7 +100,7 @@ public async Task Partial_posting_resource_produces_expected_request() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); const char euroSign = '\x20AC'; const char checkMark = '\x2713'; @@ -128,11 +128,11 @@ public async Task Partial_posting_resource_produces_expected_request() } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.SerialNumber)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PostAirplaneAsync(requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PostAirplaneAsync(requestDocument)); } // Assert @@ -163,7 +163,7 @@ public async Task Partial_patching_resource_produces_expected_request() var lastServicedAt = 1.January(2021).At(15, 23, 5, 33).ToDateTimeOffset(4.Hours()); using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new AirplanePatchRequestDocument { @@ -178,12 +178,12 @@ public async Task Partial_patching_resource_produces_expected_request() } }; - using (openApiClient.RegisterAttributesForRequestDocument(requestDocument, + using (apiClient.RegisterAttributesForRequestDocument(requestDocument, airplane => airplane.SerialNumber, airplane => airplane.LastServicedAt, airplane => airplane.IsInMaintenance, airplane => airplane.AirtimeInHours)) { // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchAirplaneAsync(airplaneId, requestDocument)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.PatchAirplaneAsync(airplaneId, requestDocument)); } // Assert @@ -215,10 +215,10 @@ public async Task Deleting_resource_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - await openApiClient.DeleteFlightAsync(flightId); + await apiClient.DeleteFlightAsync(flightId); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Delete); @@ -233,10 +233,10 @@ public async Task Getting_secondary_resource_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightPurserAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightPurserAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -252,10 +252,10 @@ public async Task Getting_secondary_resources_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightCabinCrewMembersAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCabinCrewMembersAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -271,10 +271,10 @@ public async Task Getting_ToOne_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightPurserRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightPurserRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -290,7 +290,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToOneFlightAttendantRequestData { @@ -302,7 +302,7 @@ public async Task Patching_ToOne_relationship_produces_expected_request() }; // Act - await openApiClient.PatchFlightPurserRelationshipAsync(flightId, requestDocument); + await apiClient.PatchFlightPurserRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); @@ -326,10 +326,10 @@ public async Task Getting_ToMany_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - _ = await ApiResponse.TranslateAsync(async () => await openApiClient.GetFlightCabinCrewMembersRelationshipAsync(flightId)); + _ = await ApiResponse.TranslateAsync(async () => await apiClient.GetFlightCabinCrewMembersRelationshipAsync(flightId)); // Assert wrapper.Request.Headers.GetValue(HeaderNames.Accept).Should().Be(HeaderConstants.MediaType); @@ -345,7 +345,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -365,7 +365,7 @@ public async Task Posting_ToMany_relationship_produces_expected_request() }; // Act - await openApiClient.PostFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); + await apiClient.PostFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Post); @@ -395,7 +395,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -415,7 +415,7 @@ public async Task Patching_ToMany_relationship_produces_expected_request() }; // Act - await openApiClient.PatchFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); + await apiClient.PatchFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Patch); @@ -445,7 +445,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); var requestDocument = new ToManyFlightAttendantRequestData { @@ -465,7 +465,7 @@ public async Task Deleting_ToMany_relationship_produces_expected_request() }; // Act - await openApiClient.DeleteFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); + await apiClient.DeleteFlightCabinCrewMembersRelationshipAsync(flightId, requestDocument); // Assert wrapper.Request.Method.Should().Be(HttpMethod.Delete); diff --git a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs index 6ede8a6df2..dfa5891dca 100644 --- a/test/OpenApiClientTests/LegacyClient/ResponseTests.cs +++ b/test/OpenApiClientTests/LegacyClient/ResponseTests.cs @@ -24,10 +24,10 @@ public async Task Getting_resource_collection_translates_response() const string flightServiceOnBoard = "Movies"; const string flightDepartsAt = "2014-11-25T00:00:00"; const string documentMetaValue = "1"; - const string flightMetaValue = "https://jsonapi.net/api/docs/#get-flights"; - const string purserMetaValue = "https://jsonapi.net/api/docs/#get-flight-purser"; - const string cabinCrewMembersMetaValue = "https://jsonapi.net/api/docs/#get-flight-cabin-crew-members"; - const string passengersMetaValue = "https://jsonapi.net/api/docs/#get-flight-passengers"; + const string flightMetaValue = "https://api.jsonapi.net/docs/#get-flights"; + const string purserMetaValue = "https://api.jsonapi.net/docs/#get-flight-purser"; + const string cabinCrewMembersMetaValue = "https://api.jsonapi.net/docs/#get-flight-cabin-crew-members"; + const string passengersMetaValue = "https://api.jsonapi.net/docs/#get-flight-passengers"; const string topLevelLink = HostPrefix + "flights"; const string flightResourceLink = topLevelLink + "/" + flightId; @@ -96,10 +96,10 @@ public async Task Getting_resource_collection_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightCollectionResponseDocument document = await openApiClient.GetFlightCollectionAsync(); + FlightCollectionResponseDocument document = await apiClient.GetFlightCollectionAsync(); // Assert document.Jsonapi.Should().BeNull(); @@ -118,7 +118,7 @@ public async Task Getting_resource_collection_translates_response() flight.Meta["docs"].Should().Be(flightMetaValue); flight.Attributes.FinalDestination.Should().Be(flightDestination); - flight.Attributes.StopOverDestination.Should().Be(null); + flight.Attributes.StopOverDestination.Should().BeNull(); flight.Attributes.ServicesOnBoard.Should().HaveCount(3); flight.Attributes.ServicesOnBoard.ElementAt(0).Should().Be(flightServiceOnBoard); flight.Attributes.ServicesOnBoard.ElementAt(1).Should().Be(string.Empty); @@ -172,10 +172,10 @@ public async Task Getting_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await openApiClient.GetFlightAsync(flightId); + FlightPrimaryResponseDocument document = await apiClient.GetFlightAsync(flightId); // Assert document.Jsonapi.Should().BeNull(); @@ -187,7 +187,6 @@ public async Task Getting_resource_translates_response() document.Data.Attributes.ServicesOnBoard.Should().BeNull(); document.Data.Attributes.FinalDestination.Should().BeNull(); document.Data.Attributes.StopOverDestination.Should().BeNull(); - document.Data.Attributes.OperatedBy.Should().Be(default(Airline)); } @@ -209,10 +208,10 @@ public async Task Getting_unknown_resource_translates_error_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NotFound, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func> action = async () => await openApiClient.GetFlightAsync(flightId); + Func> action = async () => await apiClient.GetFlightAsync(flightId); // Assert ExceptionAssertions assertion = await action.Should().ThrowExactlyAsync(); @@ -271,10 +270,10 @@ public async Task Posting_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.Created, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await openApiClient.PostFlightAsync(new FlightPostRequestDocument + FlightPrimaryResponseDocument document = await apiClient.PostFlightAsync(new FlightPostRequestDocument { Data = new FlightDataInPostRequest { @@ -322,10 +321,10 @@ public async Task Patching_resource_with_side_effects_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await openApiClient.PatchFlightAsync(flightId, new FlightPatchRequestDocument + FlightPrimaryResponseDocument document = await apiClient.PatchFlightAsync(flightId, new FlightPatchRequestDocument { Data = new FlightDataInPatchRequest { @@ -346,10 +345,10 @@ public async Task Patching_resource_without_side_effects_translates_response() // Arrange const string flightId = "ZvuH1"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await openApiClient.PatchFlightAsync(flightId, + FlightPrimaryResponseDocument document = await ApiResponse.TranslateAsync(async () => await apiClient.PatchFlightAsync(flightId, new FlightPatchRequestDocument { Data = new FlightDataInPatchRequest @@ -368,10 +367,10 @@ public async Task Deleting_resource_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.DeleteFlightAsync("ZvuH1"); + Func action = async () => await apiClient.DeleteFlightAsync("ZvuH1"); // Assert await action.Should().NotThrowAsync(); @@ -393,10 +392,10 @@ public async Task Getting_secondary_resource_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantSecondaryResponseDocument document = await openApiClient.GetFlightPurserAsync(flightId); + FlightAttendantSecondaryResponseDocument document = await apiClient.GetFlightPurserAsync(flightId); // Assert document.Data.Should().BeNull(); @@ -417,10 +416,10 @@ public async Task Getting_secondary_resources_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantCollectionResponseDocument document = await openApiClient.GetFlightCabinCrewMembersAsync(flightId); + FlightAttendantCollectionResponseDocument document = await apiClient.GetFlightCabinCrewMembersAsync(flightId); // Assert document.Data.Should().BeEmpty(); @@ -445,10 +444,10 @@ public async Task Getting_ToOne_relationship_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantIdentifierResponseDocument document = await openApiClient.GetFlightPurserRelationshipAsync(flightId); + FlightAttendantIdentifierResponseDocument document = await apiClient.GetFlightPurserRelationshipAsync(flightId); // Assert document.Data.Should().NotBeNull(); @@ -461,10 +460,10 @@ public async Task Patching_ToOne_relationship_translates_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - await openApiClient.PatchFlightPurserRelationshipAsync("ZvuH1", new ToOneFlightAttendantRequestData + await apiClient.PatchFlightPurserRelationshipAsync("ZvuH1", new ToOneFlightAttendantRequestData { Data = new FlightAttendantIdentifier { @@ -499,10 +498,10 @@ public async Task Getting_ToMany_relationship_translates_response() }"; using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.OK, responseBody); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - FlightAttendantIdentifierCollectionResponseDocument document = await openApiClient.GetFlightCabinCrewMembersRelationshipAsync(flightId); + FlightAttendantIdentifierCollectionResponseDocument document = await apiClient.GetFlightCabinCrewMembersRelationshipAsync(flightId); // Assert document.Data.Should().HaveCount(2); @@ -517,10 +516,10 @@ public async Task Posting_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.PostFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData + Func action = async () => await apiClient.PostFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List { @@ -546,10 +545,10 @@ public async Task Patching_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.PatchFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData + Func action = async () => await apiClient.PatchFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List { @@ -575,10 +574,10 @@ public async Task Deleting_ToMany_relationship_produces_empty_response() { // Arrange using var wrapper = FakeHttpClientWrapper.Create(HttpStatusCode.NoContent, null); - IOpenApiClient openApiClient = new OpenApiClient(wrapper.HttpClient); + IOpenApiClient apiClient = new OpenApiClient(wrapper.HttpClient); // Act - Func action = async () => await openApiClient.DeleteFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData + Func action = async () => await apiClient.DeleteFlightCabinCrewMembersRelationshipAsync("ZvuH1", new ToManyFlightAttendantRequestData { Data = new List {