216 lines
7.4 KiB
C#
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();
|
|
}
|