< Summary

Information
Line coverage
96%
Covered lines: 31
Uncovered lines: 1
Coverable lines: 32
Total lines: 103
Line coverage: 96.8%
Branch coverage
81%
Covered branches: 13
Total branches: 16
Branch coverage: 81.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ToRelativeUri(...)100%11100%
ToWebUri(...)83.33%66100%
SplitAndSanitizeSegments(...)100%22100%
SanitizePathSegment(...)75%9875%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Text/Extensions/StringExtensions.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="StringExtensions.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Generic;
 9using System.Globalization;
 10using System.Linq;
 11using MyNet.Text.Sanitization;
 12
 13#pragma warning disable IDE0130 // Namespace does not match folder structure
 14namespace MyNet.Text;
 15#pragma warning restore IDE0130 // Namespace does not match folder structure
 16
 17public static class StringExtensions
 18{
 19    private const string RelativeUriSeparator = "/";
 20    private const string WebUriSeparator = "&";
 21    private const string QueryStringEquals = "=";
 22
 23    extension(string value)
 24    {
 25        /// <summary>
 26        /// Builds a relative URI by appending sanitized non-empty path segments to the base path.
 27        /// </summary>
 28        /// <param name="parameters">Optional path segments appended to the base path.</param>
 29        /// <returns>A relative <see cref="Uri"/>.</returns>
 30        /// <exception cref="ArgumentException">Thrown when a segment is "." or "..".</exception>
 31        public Uri ToRelativeUri(params string?[] parameters)
 32        {
 933            ArgumentNullException.ThrowIfNull(parameters);
 34
 935            var allSegments = string.SplitAndSanitizeSegments(value)
 936                .Concat(parameters.SelectMany(string.SplitAndSanitizeSegments));
 37
 938            var relativePath = string.Join(RelativeUriSeparator, allSegments);
 639            return new(relativePath, UriKind.Relative);
 40        }
 41
 42        /// <summary>
 43        /// Builds an absolute web URI and appends query-string parameters safely.
 44        /// </summary>
 45        /// <param name="parameters">Query-string key/value pairs.</param>
 46        /// <returns>An absolute <see cref="Uri"/>.</returns>
 47        /// <exception cref="ArgumentException">Thrown when the base URI is not absolute.</exception>
 48        public Uri ToWebUri(params (string Key, string Value)[] parameters)
 49        {
 950            ArgumentNullException.ThrowIfNull(parameters);
 951            if (!Uri.TryCreate(value, UriKind.Absolute, out var baseUri))
 52            {
 353                throw new ArgumentException("The base URI must be absolute.");
 54            }
 55
 656            var builder = new UriBuilder(baseUri);
 657            var currentQuery = builder.Query.TrimStart('?');
 658            var appendedQuery = string.Join(
 659                WebUriSeparator,
 660                parameters
 661                    .Where(x => !string.IsNullOrWhiteSpace(x.Key))
 662                    .Select(x => $"{Sanitizer.UrlSegment.Apply(x.Key, CultureInfo.InvariantCulture)}{QueryStringEquals}{
 63
 664            builder.Query = string.IsNullOrEmpty(currentQuery)
 665                ? appendedQuery
 666                : string.IsNullOrEmpty(appendedQuery)
 667                    ? currentQuery
 668                    : currentQuery + WebUriSeparator + appendedQuery;
 69
 670            return builder.Uri;
 71        }
 72
 73        /// <summary>
 74        /// Splits the input string into segments based on the defined separator, trims whitespace, removes empty segmen
 75        /// </summary>
 76        /// <param name="path">The input string to split and sanitize.</param>
 77        /// <returns>An enumerable of sanitized segments.</returns>
 78        private static IEnumerable<string> SplitAndSanitizeSegments(string? path) =>
 3079            string.IsNullOrWhiteSpace(path)
 3080                ? []
 3081                : path
 3082                    .Split([RelativeUriSeparator], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntrie
 3083                    .Select(SanitizePathSegment)
 3084                    .Where(x => !string.IsNullOrEmpty(x))
 3085                    .Select(x => x!);
 86
 87        /// <summary>
 88        /// Sanitizes a path segment by trimming whitespace and applying URL-safe sanitization. If the segment is null, 
 89        /// </summary>
 90        /// <param name="segment">The path segment to sanitize.</param>
 91        /// <returns>The sanitized path segment, or null if the segment is null, empty, or consists only of whitespace.<
 92        /// <exception cref="ArgumentException">Thrown when the segment is "." or "..".</exception>
 93        private static string? SanitizePathSegment(string? segment)
 94        {
 2495            if (string.IsNullOrWhiteSpace(segment))
 096                return null;
 97
 2498            var normalized = segment.Trim();
 2499            return normalized is "." or ".." ? throw new ArgumentException("Path segments '.' and '..' are not allowed."
 100        }
 101    }
 102}
 103