packages feed

link-canonical-0.1.0.0: src/Link/Canonical.hs

-- |
-- Module      : Link.Canonical
-- Description : URL canonicalization library for semantic link identity
-- Copyright   : (c) Nadeem Bitar, 2026
-- License     : MIT
-- Maintainer  : Nadeem Bitar
-- Stability   : experimental
--
-- This library converts arbitrary URLs into canonical, semantically stable
-- identifiers. It handles:
--
-- * Redirect resolution (following URL shorteners, etc.)
-- * Tracking parameter removal (utm_*, fbclid, etc.)
-- * Domain-specific normalization (YouTube, Amazon, Twitter, etc.)
-- * Generic URL normalization (scheme, host, port, path, query, fragment)
--
-- = Quick Start
--
-- @
-- import Link.Canonical
-- import Text.URI (mkURI)
--
-- main :: IO ()
-- main = do
--   let Right uri = mkURI "https://youtu.be/dQw4w9WgXcQ?utm_source=share"
--   result <- normalizeWithDefaults uri
--   case result of
--     Left err -> print err
--     Right canonical -> print canonical
-- @
--
-- = Layered API
--
-- The library provides three API layers:
--
-- 1. __Pure normalization__ ('normalizeUri'): No IO, just transforms URIs
-- 2. __With redirect resolution__ ('normalizeLink'): Follows redirects, then normalizes
-- 3. __Convenient defaults__ ('normalizeWithDefaults'): Uses default configuration
module Link.Canonical
  ( -- * Main API
    normalizeLink,
    normalizeWithDefaults,
    normalizeUri,

    -- * Configuration
    NormConfig (..),
    RedirectConfig (..),
    TrackingConfig (..),
    TrailingSlash (..),
    defaultConfig,
    defaultRedirectConfig,
    defaultTrackingConfig,
    defaultTrackingParams,

    -- * Result types
    NormResult (..),
    NormTrace (..),

    -- * Error types
    NormError (..),
    RedirectError (..),
    HttpClientError (..),

    -- * HTTP client
    HttpClient (..),
    HttpResponse (..),
    HttpClientIO (..),
    newHttpClientIO,

    -- * Domain rules
    DomainRule (..),
    domainRule,
    applyDomainRules,
    defaultDomainRules,

    -- * Observability
    NormHooks (..),

    -- * Re-exports
    URI,
    mkURI,
  )
where

import Link.Canonical.Config
import Link.Canonical.Error
import Link.Canonical.Http
import Link.Canonical.Normalize (normalizeUri, normalizeUriWithTrace)
import Link.Canonical.Prelude
import Link.Canonical.Redirect (resolveFinalUriWithChain)
import Link.Canonical.Rules
import Link.Canonical.Types

-- | Normalize a URL with full redirect resolution
--
-- This is the main entry point for URL normalization. It:
--
-- 1. Resolves redirects (following 3xx responses)
-- 2. Applies generic normalization (scheme, host, port, path, query, fragment)
-- 3. Applies domain-specific rules (YouTube, Amazon, etc.)
-- 4. Strips tracking parameters
--
-- @
-- config = defaultConfig
--   & #redirects . #maxRedirects .~ 5
--   & #tracking . #allowlist .~ Set.fromList ["ref"]
--
-- result <- normalizeLink config httpClient uri
-- @
normalizeLink ::
  (HttpClient m) =>
  NormConfig ->
  URI ->
  m (Either NormError NormResult)
normalizeLink config uri = do
  -- Step 1: Resolve redirects
  redirectResult <- resolveFinalUriWithChain (config ^. #redirects) uri

  case redirectResult of
    Left err -> pure $ Left $ RedirectError err
    Right (resolved, chain) -> do
      -- Step 2: Apply normalization (generic + domain rules)
      let (canonical, ruleApplied, strippedParams) =
            normalizeUriWithTrace config defaultDomainRules resolved

      pure $
        Right $
          NormResult
            { canonical = canonical,
              original = uri,
              redirectsCount = length chain,
              redirectChain = chain,
              ruleApplied = ruleApplied,
              paramsStripped = strippedParams
            }

-- | Normalize a URL using default configuration
--
-- Convenience function that uses 'defaultConfig' and the default HTTP client.
--
-- @
-- result <- normalizeWithDefaults uri
-- case result of
--   Left err -> handleError err
--   Right res -> useCanonical (res ^. #canonical)
-- @
normalizeWithDefaults ::
  (HttpClient m) =>
  URI ->
  m (Either NormError URI)
normalizeWithDefaults uri = do
  result <- normalizeLink defaultConfig uri
  pure $ fmap (^. #canonical) result