packages feed

uri-templater-1.0.0: src/Network/URI/Template/Parser.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeSynonymInstances #-}

module Network.URI.Template.Parser (
  parseTemplate,
) where

import qualified Data.ByteString as BS
import qualified Data.Char as C
import qualified Data.String as S
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Vector as V
import FlatParse.Basic
import Network.URI.Template.Error
import Network.URI.Template.Types


-- | Parse a character in a specific range
{-# INLINE charRange #-}
charRange :: Char -> Char -> Parser e Char
charRange l r = satisfy (\c -> l <= c && c <= r)


-- | Parse a character matching any of the given ranges
-- Uses fusedSatisfy for better performance when possible
charRanges :: [(Char, Char)] -> Parser e Char
charRanges [] = empty
charRanges ((l, r) : rest) = satisfy (\c -> l <= c && c <= r) <|> charRanges rest


-- | Parse Unicode characters as specified in RFC 6570
ucschar :: Parser e Char
ucschar =
  charRanges
    [ ('\xA0', '\xD7FF')
    , ('\xF900', '\xFDCF')
    , ('\xFDF0', '\xFFEF')
    , ('\x10000', '\x1FFFD')
    , ('\x20000', '\x2FFFD')
    , ('\x30000', '\x3FFFD')
    , ('\x40000', '\x4FFFD')
    , ('\x50000', '\x5FFFD')
    , ('\x60000', '\x6FFFD')
    , ('\x70000', '\x7FFFD')
    , ('\x80000', '\x8FFFD')
    , ('\x90000', '\x9FFFD')
    , ('\xA0000', '\xAFFFD')
    , ('\xB0000', '\xBFFFD')
    , ('\xC0000', '\xCFFFD')
    , ('\xD0000', '\xDFFFD')
    , ('\xE1000', '\xEFFFD')
    ]


-- | Parse private use area characters
iprivate :: Parser e Char
iprivate =
  charRanges
    [ ('\xE000', '\xF8FF')
    , ('\xF0000', '\xFFFFD')
    , ('\x100000', '\x10FFFD')
    ]


-- | Parse a percent-encoded sequence
{-# INLINE pctEncoded #-}
pctEncoded :: Parser e String
pctEncoded = do
  $(char '%')
  d1 <- hexDigit
  d2 <- hexDigit
  return ['%', d1, d2]
 where
  {-# INLINE hexDigit #-}
  hexDigit = satisfy (\c -> C.isDigit c || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))


-- | Parse a literal character
literalChar :: Parser e Char
literalChar =
  ($(char '\x21') >> pure '\x21')
    <|> ($(char '\x23') >> pure '\x23')
    <|> ($(char '\x24') >> pure '\x24')
    <|> ($(char '\x26') >> pure '\x26')
    <|> ($(char '\x3D') >> pure '\x3D')
    <|> ($(char '\x5D') >> pure '\x5D')
    <|> ($(char '\x5F') >> pure '\x5F')
    <|> ($(char '\x7E') >> pure '\x7E')
    <|> charRanges [('\x28', '\x3B'), ('\x3F', '\x5B'), ('\x61', '\x7A')]
    <|> ucschar
    <|> iprivate


-- | Parse a literal segment
-- Uses byteStringOf to avoid intermediate list allocations
{-# INLINE literal #-}
literal :: Parser e TemplateSegment
literal = Literal . TE.decodeUtf8 <$> byteStringOf (skipSome literalCharOrPct)
 where
  {-# INLINE literalCharOrPct #-}
  literalCharOrPct = (() <$ literalChar) <|> (() <$ pctEncoded)


-- | Parse variables in an embed
{-# INLINE variables #-}
variables :: Parser e TemplateSegment
variables = do
  mod <- modifier
  vars <- sepBy1 variable (ws *> $(char ',') *> ws)
  return $ Embed mod vars


-- | Parse optional whitespace
{-# INLINE ws #-}
ws :: Parser e ()
ws = skipMany (satisfy (\c -> c == ' ' || c == '\t' || c == '\n' || c == '\r'))


-- | Parse a modifier character
modifier :: Parser e Modifier
modifier =
  ($(char '+') *> pure Reserved)
    <|> ($(char '#') *> pure Fragment)
    <|> ($(char '.') *> pure Label)
    <|> ($(char '/') *> pure PathSegment)
    <|> ($(char ';') *> pure PathParameter)
    <|> ($(char '?') *> pure Query)
    <|> ($(char '&') *> pure QueryContinuation)
    <|> ($(char '=') *> pure Reserved)
    <|> ($(char '@') *> pure Reserved)
    <|> ($(char '!') *> pure Reserved)
    <|> ($(char '|') *> pure Reserved)
    <|> pure Simple


-- | Parse a variable
variable :: Parser e Variable
variable = do
  nm <- name
  valMod <- valueModifier
  return $ Variable nm valMod
 where
  {-# INLINE name #-}
  name = TE.decodeUtf8 <$> byteStringOf (skipSome nameChar)
   where
    {-# INLINE nameChar #-}
    nameChar = (() <$ alphaNum) <|> (() <$ $(char '_')) <|> (() <$ pctEncoded)
  {-# INLINE valueModifier #-}
  valueModifier =
    ($(char '*') *> pure Explode)
      <|> ($(char ':') *> (MaxLength <$> parseInt))
      <|> pure Normal
  {-# INLINE parseInt #-}
  parseInt = do
    digits <- some digit
    return $ read digits
  {-# INLINE alphaNum #-}
  alphaNum = satisfy (\c -> C.isAlphaNum c)
  {-# INLINE digit #-}
  digit = satisfy C.isDigit


-- | Parse an embedded variable expression
{-# INLINE embed #-}
embed :: Parser e TemplateSegment
embed = $(char '{') *> variables <* $(char '}')


-- | Parse a URI template segments
{-# INLINE uriTemplate #-}
uriTemplate :: Parser e (V.Vector TemplateSegment)
uriTemplate = V.fromList <$> (ws *> many (literal <|> embed))


-- | Helper to separate a parser by a separator
{-# INLINE sepBy1 #-}
sepBy1 :: Parser e a -> Parser e sep -> Parser e [a]
sepBy1 p sep = (:) <$> p <*> many (sep *> p)


-- | Parse a template from a String
parseTemplate :: String -> Either ParseError UriTemplate
parseTemplate input =
  case runParser uriTemplate (TE.encodeUtf8 $ T.pack input) of
    OK result _ -> Right (UriTemplate result)
    Err _ -> Left (GenericParseError ("Parse error in URI template: " ++ input))
    Fail -> Left (GenericParseError ("Parse error in URI template: " ++ input))


{- | 'IsString' instance for 'UriTemplate' allows using string literals directly as templates
when @OverloadedStrings@ is enabled.

>>> :set -XOverloadedStrings
>>> let template = "/users/{userId}" :: UriTemplate
>>> renderTemplate template
"/users/{userId}"

Note: If the string fails to parse as a valid URI template, this will call 'error'.
For fallible parsing, use 'parseTemplate' instead.
-}
instance S.IsString UriTemplate where
  fromString s = case parseTemplate s of
    Right template -> template
    Left err -> error $ "Invalid URI template: " ++ s ++ "\n" ++ show err