< Summary - Code Coverage

Information
Class: Plainquire.Sort.Swashbuckle.OpenApiOperationExtensions
Assembly: Plainquire.Sort.Swashbuckle
File(s): /home/runner/work/plainquire/plainquire/Plainquire.Sort/Plainquire.Sort.Swashbuckle/Extensions/OpenApiOperationExtensions.cs
Tag: 74_23635074410
Line coverage
94%
Covered lines: 102
Uncovered lines: 6
Coverable lines: 108
Total lines: 192
Line coverage: 94.4%
Branch coverage
71%
Covered branches: 23
Total branches: 32
Branch coverage: 71.8%
Method coverage
100%
Covered methods: 10
Total methods: 10
Method coverage: 100%

Metrics

File(s)

/home/runner/work/plainquire/plainquire/Plainquire.Sort/Plainquire.Sort.Swashbuckle/Extensions/OpenApiOperationExtensions.cs

#LineLine coverage
 1using Microsoft.OpenApi;
 2using Plainquire.Filter.Abstractions;
 3using Plainquire.Sort.Swashbuckle.Models;
 4using Swashbuckle.AspNetCore.SwaggerGen;
 5using System;
 6using System.Collections.Generic;
 7using System.Linq;
 8using System.Reflection;
 9using System.Text.Json.Nodes;
 10using System.Text.RegularExpressions;
 11
 12namespace Plainquire.Sort.Swashbuckle;
 13
 14/// <summary>
 15/// Extension methods for <see cref="OpenApiOperation"/>.
 16/// </summary>
 17internal static class OpenApiOperationExtensions
 18{
 19    private const string PARAMETER_INDEX_EXTENSION = "x-original-parameter-index";
 20    private const string ENTITY_SORT_EXTENSION = "x-entity-sort";
 21    private const string ENTITY_DELETE_EXTENSION = "x-entity-sort-delete";
 22
 23    /// <summary>
 24    /// Replaces <see cref="EntitySort{TEntity}"/> and with the sort parameters.
 25    /// </summary>
 26    /// <param name="operation">The <see cref="OpenApiOperation"/> to operate on.</param>
 27    /// <param name="parametersToReplace">The parameters to replace.</param>
 28    public static void ReplaceSortParameters(this OpenApiOperation operation, IList<SortParameterReplacement> parameters
 29    {
 6230        MarkExistingParametersForDeletion(parametersToReplace);
 31
 6232        var httpQueryParameterGroup = GroupByHttpQueryParameterName(parametersToReplace);
 21033        foreach (var (queryParameter, parameters) in httpQueryParameterGroup)
 34        {
 4335            var (prefixes, postfixes, primaryAscendingPostfix, primaryDescendingPostfix) = GetSortPrefixes(parameters);
 4336            var allowedPropertyNamePattern = CreatePropertyNamePattern(parameters, prefixes, postfixes);
 37
 4338            var openApiParameter = new OpenApiParameter
 4339            {
 4340                Name = queryParameter,
 4341                Description = $"Sorts the result by the given property in ascending ({primaryAscendingPostfix}) or desce
 4342                Schema = new OpenApiSchema
 4343                {
 4344                    Type = JsonSchemaType.Array,
 4345                    Items = new OpenApiSchema
 4346                    {
 4347                        Type = JsonSchemaType.String,
 4348                        Example = string.Empty,
 4349                        Pattern = allowedPropertyNamePattern
 4350                    },
 4351                },
 4352                In = ParameterLocation.Query,
 4353                Extensions = new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnoreCase)
 4354                {
 4355                    [ENTITY_SORT_EXTENSION] = new JsonNodeExtension(JsonValue.Create(true))
 4356                }
 4357            };
 58
 4359            operation.Parameters ??= new List<IOpenApiParameter>();
 4360            var insertionIndex = operation.Parameters.IndexOf(parameters[0].OpenApiParameter);
 4361            operation.Parameters.Insert(insertionIndex, openApiParameter);
 62        }
 63
 6264        RemoveParametersMarkedForDeletion(operation);
 6265    }
 66
 67    public static void AddOriginalIndexExtensionIfMissing(this OpenApiOperation operation, OperationFilterContext contex
 68    {
 6269        if (operation.Parameters == null)
 070            return;
 71
 6272        var indexExtensionsAlreadyAdded = operation.Parameters.Any(p => p.Extensions?.ContainsKey(PARAMETER_INDEX_EXTENS
 6273        if (indexExtensionsAlreadyAdded)
 3174            return;
 75
 22676        for (var index = 0; index < operation.Parameters.Count; index++)
 77        {
 8278            var parameter = operation.Parameters[index];
 8279            if (parameter is not IOpenApiExtensible openApiParameter)
 080                throw new InvalidOperationException("The OpenApiParameter must implement IOpenApiExtensible to be replac
 81
 8282            openApiParameter.Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnoreCase);
 8283            openApiParameter.Extensions.Add(PARAMETER_INDEX_EXTENSION, new JsonNodeExtension(JsonValue.Create(index)));
 84        }
 3185    }
 86
 87    public static int GetOriginalIndex(this IOpenApiParameter parameter)
 88    {
 28689        if (parameter.Extensions == null)
 090            return -1;
 91
 28692        if (!parameter.Extensions.TryGetValue(PARAMETER_INDEX_EXTENSION, out var extension))
 5093            return -1;
 94
 23695        if (extension is not JsonNodeExtension nodeExtension)
 096            throw new InvalidOperationException($"Extension '{PARAMETER_INDEX_EXTENSION}' must be of type {nameof(JsonNo
 97
 23698        if (nodeExtension.Node is not JsonValue jsonValue)
 099            throw new InvalidOperationException($"Value of extension '{PARAMETER_INDEX_EXTENSION}' must be of type {name
 100
 236101        return jsonValue.GetValue<int>();
 102    }
 103
 104    private static Dictionary<string, List<SortParameterReplacement>> GroupByHttpQueryParameterName(IList<SortParameterR
 62105        => parametersToReplace
 62106            .GroupBy(parameter =>
 62107            {
 62108                var bindingParameterName = parameter.OpenApiDescription.ParameterDescriptor.BindingInfo?.BinderModelName
 62109                var actionParameterName = parameter.OpenApiDescription.ParameterDescriptor.Name;
 62110                return bindingParameterName ?? actionParameterName;
 62111            }, StringComparer.OrdinalIgnoreCase)
 62112            .ToDictionary(
 62113                group => group.Key,
 62114                group => group.ToList(),
 62115                StringComparer.OrdinalIgnoreCase
 62116            );
 117
 118    private static void MarkExistingParametersForDeletion(IList<SortParameterReplacement> parameters)
 119    {
 330120        foreach (var parameter in parameters)
 121        {
 103122            if (parameter.OpenApiParameter is not IOpenApiExtensible extensibleParameter)
 0123                throw new InvalidOperationException("The OpenApiParameter must implement IOpenApiExtensible to be replac
 124
 103125            extensibleParameter.Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnoreCas
 103126            extensibleParameter.Extensions.TryAdd(ENTITY_DELETE_EXTENSION, new JsonNodeExtension(JsonValue.Create(true))
 127        }
 62128    }
 129
 130    private static void RemoveParametersMarkedForDeletion(OpenApiOperation operation)
 131    {
 62132        operation.Parameters ??= new List<IOpenApiParameter>();
 62133        var parametersToRemove = operation.Parameters
 62134            .Where(parameter =>
 62135            {
 62136                if (parameter is not IOpenApiExtensible extensibleParameter)
 62137                    throw new InvalidOperationException("The OpenApiParameter must implement IOpenApiExtensible to be re
 62138
 62139                extensibleParameter.Extensions ??= new Dictionary<string, IOpenApiExtension>(StringComparer.OrdinalIgnor
 62140
 62141                return extensibleParameter.Extensions.ContainsKey(ENTITY_DELETE_EXTENSION);
 62142            })
 62143            .ToList();
 144
 310145        foreach (var parameter in parametersToRemove)
 93146            operation.Parameters.Remove(parameter);
 62147    }
 148
 149    private static (List<string>, List<string>, string, string) GetSortPrefixes(IReadOnlyCollection<SortParameterReplace
 150    {
 43151        var ascendingPrefixes = parameters.Select(parameter => parameter.Configuration.AscendingPrefixes).SelectMany(x =
 43152        var descendingPrefixes = parameters.Select(parameter => parameter.Configuration.DescendingPrefixes).SelectMany(x
 43153        var ascendingPostfixes = parameters.Select(parameter => parameter.Configuration.AscendingPostfixes).SelectMany(x
 43154        var descendingPostfixes = parameters.Select(parameter => parameter.Configuration.DescendingPostfixes).SelectMany
 155
 43156        var prefixes = ascendingPrefixes.Concat(descendingPrefixes).Distinct(StringComparer.OrdinalIgnoreCase).Select(Re
 43157        var postfixes = ascendingPostfixes.Concat(descendingPostfixes).Distinct(StringComparer.OrdinalIgnoreCase).Select
 158
 43159        var primaryAscendingPostfix = ascendingPostfixes[0];
 43160        var primaryDescendingPostfix = descendingPostfixes[0];
 161
 43162        return (prefixes, postfixes, primaryAscendingPostfix, primaryDescendingPostfix);
 163    }
 164
 165    private static string CreatePropertyNamePattern(IEnumerable<SortParameterReplacement> parameters, IEnumerable<string
 166    {
 43167        var sortablePropertyNames = GetSortablePropertyNames(parameters);
 168
 43169        var allowedPropertyNamePattern = $"^({string.Join("|", prefixes)})?({string.Join("|", sortablePropertyNames)})(\
 43170        return allowedPropertyNamePattern;
 171    }
 172
 173    private static List<string> GetSortablePropertyNames(IEnumerable<SortParameterReplacement> parameters)
 43174        => parameters
 43175            .Select(parameter => parameter.SortedType)
 43176            .SelectMany(sortedType => sortedType.GetSortPropertyNames())
 43177            .Distinct(StringComparer.OrdinalIgnoreCase)
 43178            .Select(Regex.Escape)
 43179            .ToList();
 180
 181    private static List<string> GetSortPropertyNames(this Type sortedType)
 182    {
 103183        var entityFilterAttribute = sortedType.GetCustomAttribute<EntityFilterAttribute>();
 184
 103185        var sortableProperties = sortedType
 103186            .GetSortableProperties()
 103187            .Select(property => property.GetSortParameterName(entityFilterAttribute?.Prefix))
 103188            .ToList();
 189
 103190        return sortableProperties;
 191    }
 192}