Files
2025-10-27 21:39:50 -04:00

216 lines
7.4 KiB
C#

using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Primitives;
namespace Prefab.Web.UrlPolicy;
/// <summary>
/// Provides centralized helpers for building canonical catalog URLs used by the BFF.
/// </summary>
public static class UrlPolicy
{
public const int DefaultPage = 1;
public const int DefaultPageSize = 24;
public const string DefaultSort = "name:asc";
public const string DefaultView = "grid";
private static readonly int[] AllowedPageSizes = [12, 24, 48];
/// <summary>
/// Builds a canonical catalog URL for navigating to a category landing page.
/// </summary>
/// <param name="slug">The category slug to embed in the query string.</param>
public static string BrowseCategory(string slug)
{
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
return $"/catalog/category?category-slug={Uri.EscapeDataString(slug)}";
}
/// <summary>
/// Builds a canonical catalog URL for navigating to the products grid for a category.
/// </summary>
/// <param name="categorySlug">The slug of the category whose products should be displayed.</param>
public static string BrowseProducts(string categorySlug)
{
ArgumentException.ThrowIfNullOrWhiteSpace(categorySlug);
return $"/catalog/products?category-slug={Uri.EscapeDataString(categorySlug)}";
}
/// <summary>
/// Builds a canonical catalog URL for navigating to a specific product detail page.
/// </summary>
/// <param name="slug">The slug of the product.</param>
public static string BrowseProduct(string slug)
{
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
return $"/browse/product?slug={Uri.EscapeDataString(slug)}";
}
/// <summary>
/// Builds a canonical catalog URL for the category browsing experience including paging state.
/// </summary>
public static string Category(
string slug,
int page = DefaultPage,
int pageSize = DefaultPageSize,
string? sort = null,
string? view = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(slug);
var normalizedPage = NormalizePage(page);
var normalizedPageSize = NormalizePageSize(pageSize);
var normalizedSort = NormalizeSort(sort);
var normalizedView = NormalizeView(view);
var parameters = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["slug"] = slug,
["page"] = normalizedPage.ToString(CultureInfo.InvariantCulture),
["pageSize"] = normalizedPageSize.ToString(CultureInfo.InvariantCulture),
["sort"] = normalizedSort,
["view"] = normalizedView
};
return QueryHelpers.AddQueryString("/catalog/category", parameters);
}
/// <summary>
/// Attempts to parse the catalog category browse URL parameters.
/// </summary>
public static bool TryParseCategory(
Uri uri,
out string slug,
out int page,
out int pageSize,
out string sort,
out string view)
{
ArgumentNullException.ThrowIfNull(uri);
slug = string.Empty;
page = DefaultPage;
pageSize = DefaultPageSize;
sort = DefaultSort;
view = DefaultView;
var queryValues = ParseQuery(uri);
if (!queryValues.TryGetValue("slug", out var slugValues))
{
return false;
}
var candidateSlug = slugValues.ToString();
if (string.IsNullOrWhiteSpace(candidateSlug))
{
return false;
}
slug = candidateSlug;
page = NormalizePage(ParseInt(queryValues, "page", DefaultPage));
pageSize = NormalizePageSize(ParseInt(queryValues, "pageSize", DefaultPageSize));
sort = NormalizeSort(queryValues.TryGetValue("sort", out var sortValues) ? sortValues.ToString() : null);
view = NormalizeView(queryValues.TryGetValue("view", out var viewValues) ? viewValues.ToString() : null);
return true;
}
/// <summary>
/// Builds a canonical catalog URL for browsing products without a category context.
/// </summary>
public static string Products(
int page = DefaultPage,
int pageSize = DefaultPageSize,
string? sort = null,
string? view = null)
{
var normalizedPage = NormalizePage(page);
var normalizedPageSize = NormalizePageSize(pageSize);
var normalizedSort = NormalizeSort(sort);
var normalizedView = NormalizeView(view);
var parameters = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase)
{
["page"] = normalizedPage.ToString(CultureInfo.InvariantCulture),
["pageSize"] = normalizedPageSize.ToString(CultureInfo.InvariantCulture),
["sort"] = normalizedSort,
["view"] = normalizedView
};
return QueryHelpers.AddQueryString("/catalog/products", parameters);
}
/// <summary>
/// Attempts to parse the products browse URL parameters.
/// </summary>
public static bool TryParseProducts(
Uri uri,
out int page,
out int pageSize,
out string sort,
out string view)
{
ArgumentNullException.ThrowIfNull(uri);
page = DefaultPage;
pageSize = DefaultPageSize;
sort = DefaultSort;
view = DefaultView;
var queryValues = ParseQuery(uri);
page = NormalizePage(ParseInt(queryValues, "page", DefaultPage));
pageSize = NormalizePageSize(ParseInt(queryValues, "pageSize", DefaultPageSize));
sort = NormalizeSort(queryValues.TryGetValue("sort", out var sortValues) ? sortValues.ToString() : null);
view = NormalizeView(queryValues.TryGetValue("view", out var viewValues) ? viewValues.ToString() : null);
return true;
}
private static IDictionary<string, StringValues> ParseQuery(Uri uri)
{
var querySegment = uri.IsAbsoluteUri ? uri.Query : uri.OriginalString;
if (string.IsNullOrEmpty(querySegment))
{
return new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
}
if (!uri.IsAbsoluteUri)
{
var questionIndex = querySegment.IndexOf('?', StringComparison.Ordinal);
querySegment = questionIndex >= 0 ? querySegment[questionIndex..] : string.Empty;
}
if (string.IsNullOrEmpty(querySegment))
{
return new Dictionary<string, StringValues>(StringComparer.OrdinalIgnoreCase);
}
return QueryHelpers.ParseQuery(querySegment);
}
private static int ParseInt(IDictionary<string, StringValues> values, string key, int fallback)
{
if (values.TryGetValue(key, out var raw) &&
int.TryParse(raw.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
{
return parsed;
}
return fallback;
}
private static int NormalizePage(int page) => page < 1 ? DefaultPage : page;
private static int NormalizePageSize(int pageSize) =>
AllowedPageSizes.Contains(pageSize) ? pageSize : DefaultPageSize;
private static string NormalizeSort(string? sort) =>
string.IsNullOrWhiteSpace(sort) ? DefaultSort : sort.Trim();
private static string NormalizeView(string? view) =>
string.IsNullOrWhiteSpace(view) ? DefaultView : view.Trim().ToLowerInvariant();
}