packages feed

xrefcheck-0.3.0: src/Xrefcheck/Data/URI.hs

{- SPDX-FileCopyrightText: 2023 Serokell <https://serokell.io>
 -
 - SPDX-License-Identifier: MPL-2.0
 -}

{-# LANGUAGE ExistentialQuantification #-}

module Xrefcheck.Data.URI
  ( UriParseError (..)
  , parseUri
  ) where

import Universum

import Control.Exception.Safe (handleJust)
import Control.Monad.Except (throwError)
import Text.URI (ParseExceptionBs, URI, mkURIBs)
import URI.ByteString qualified as URIBS

data UriParseError
  = UPEInvalid URIBS.URIParseError
  | UPEConversion ParseExceptionBs
  deriving stock (Show, Eq)

data AnyURIRef = forall a. AnyURIRef (URIBS.URIRef a)

serializeAnyURIRef :: AnyURIRef -> ByteString
serializeAnyURIRef (AnyURIRef uri) = URIBS.serializeURIRef' uri

-- | Parse URI according to RFC 3986 extended by allowing non-encoded
-- `[` and `]` in query string.
--
-- The first parameter indicates whether the parsing should admit relative
-- URIs or not.
parseUri :: Bool -> Text -> ExceptT UriParseError IO URI
parseUri canBeRelative link = do
  -- There exist two main standards of URL parsing: RFC 3986 and the Web
  -- Hypertext Application Technology Working Group's URL standard. Ideally,
  -- we want to be able to parse the URLs in accordance with the latter
  -- standard, because it provides a much less ambiguous set of rules for
  -- percent-encoding special characters, and is essentially a living
  -- standard that gets updated constantly.
  --
  -- We have chosen the 'uri-bytestring' library for URI parsing because
  -- of the 'laxURIParseOptions' parsing configuration. 'mkURI' from
  -- the 'modern-uri' library parses URIs in accordance with RFC 3986 and does
  -- not provide a means of parsing customization, which contrasts with
  -- 'parseURI' that accepts a 'URIParserOptions'. One of the predefined
  -- configurations of this type is 'strictURIParserOptions', which follows
  -- RFC 3986, and the other -- 'laxURIParseOptions' -- allows brackets
  -- in the queries, which draws us closer to the WHATWG URL standard.
  --
  -- The 'modern-uri' package can parse an URI deciding if it is absolute or
  -- relative depending on the success or failure of the scheme parsing. By
  -- contrast, in 'uri-bytestring' it has to be decided beforehand, resulting in
  -- different URI types.
  uri <- case URIBS.parseURI URIBS.laxURIParserOptions (encodeUtf8 link) of
    Left (URIBS.MalformedScheme _) | canBeRelative ->
      URIBS.parseRelativeRef URIBS.laxURIParserOptions (encodeUtf8 link)
        & either (throwError . UPEInvalid) (pure . AnyURIRef)
    Left err -> throwError $ UPEInvalid err
    Right uri -> pure $ AnyURIRef uri

  -- We stick to our infrastructure by continuing to operate on the datatypes
  -- from 'modern-uri', which are used in the 'req' library. First we
  -- serialize our URI parsed with 'parseURI' so it becomes a 'ByteString'
  -- with all the necessary special characters *percent-encoded*, and then
  -- call 'mkURIBs'.
  mkURIBs (serializeAnyURIRef uri)
    -- Ideally, this exception should never be thrown, as the URI
    -- already *percent-encoded* with 'parseURI' from 'uri-bytestring'
    -- and 'mkURIBs' is only used to convert to 'URI' type from
    -- 'modern-uri' package.
    & handleJust fromException (throwError . UPEConversion)