uri-templater-1.0.0: src/Network/URI/Template/Internal.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Network.URI.Template.Internal where
import Control.Monad.Writer.Strict
import qualified Data.ByteString.Builder as BB
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as BL
import Data.DList hiding (map)
import Data.List (intersperse)
import Data.Maybe
import Data.Monoid
import Data.Proxy
import qualified Data.Text as T
import Data.Text.Encoding
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Builder as TB
import qualified Data.Text.Lazy.Encoding as TL
import qualified Data.Vector as V
import Network.HTTP.Types.URI
import Network.URI.Template.Types
import Web.HttpApiData (toUrlPiece)
import Web.Internal.HttpApiData (ToHttpApiData (..))
class Monoid (Builder a) => Buildable a where
type Builder a
-- | Convert the intermediate output into the end result
build :: Builder a -> a
-- | Construct an appendable character representation
addChar :: Proxy a -> Char -> Builder a
-- | Construct an appendable string representation
addString :: Proxy a -> T.Text -> Builder a
-- | Convert a bytestring builder into the appropriate format
addBytestringBuilder :: Proxy a -> BB.Builder -> Builder a
instance Buildable String where
type Builder String = DList Char
build = Data.DList.toList
addChar _ = singleton
addString _ = fromList . T.unpack
addBytestringBuilder _ = fromList . BL.unpack . BB.toLazyByteString
instance Buildable BS.ByteString where
type Builder BS.ByteString = BB.Builder
build = BL.toStrict . BB.toLazyByteString
addChar _ = BB.char8
addString _ = BB.byteString . T.encodeUtf8
addBytestringBuilder _ = id
instance Buildable BL.ByteString where
type Builder BL.ByteString = BB.Builder
build = BB.toLazyByteString
addChar _ = BB.char8
addString _ = BB.byteString . T.encodeUtf8
addBytestringBuilder _ = id
instance Buildable T.Text where
type Builder T.Text = TB.Builder
build = TL.toStrict . TB.toLazyText
addChar _ = TB.singleton
addString _ = TB.fromText
addBytestringBuilder _ = TB.fromLazyText . TL.decodeUtf8 . BB.toLazyByteString
instance Buildable TL.Text where
type Builder TL.Text = TB.Builder
build = TB.toLazyText
addChar _ = TB.singleton
addString _ = TB.fromText
addBytestringBuilder _ = TB.fromLazyText . TL.decodeUtf8 . BB.toLazyByteString
instance Buildable BB.Builder where
type Builder BB.Builder = BB.Builder
build = id
addChar _ = BB.char8
addString _ = BB.byteString . T.encodeUtf8
addBytestringBuilder _ = id
instance Buildable TB.Builder where
type Builder TB.Builder = TB.Builder
build = id
addChar _ = TB.singleton
addString _ = TB.fromText
addBytestringBuilder _ = TB.fromLazyText . TL.decodeUtf8 . BB.toLazyByteString
-- instance Buildable (Path, QueryParams, Hash)
data Allow = Unreserved | UnreservedOrReserved
allowEncoder :: (Buildable str, ToHttpApiData a) => Proxy str -> Allow -> a -> Builder str
allowEncoder p Unreserved = addBytestringBuilder p . urlEncodeBuilder True . T.encodeUtf8 . toUrlPiece
allowEncoder p UnreservedOrReserved = addString p . toUrlPiece
data ProcessingOptions = ProcessingOptions
{ modifierPrefix :: Maybe Char
, modifierSeparator :: Char
, modifierSupportsNamed :: Bool
, modifierIfEmpty :: Maybe Char
, modifierAllow :: Allow
}
type BoundValue = (T.Text, WrappedValue)
option :: Maybe Char -> Char -> Bool -> Maybe Char -> Allow -> ProcessingOptions
option = ProcessingOptions
options :: Modifier -> ProcessingOptions
options m = case m of
Simple -> option Nothing ',' False Nothing Unreserved
Reserved -> option Nothing ',' False Nothing UnreservedOrReserved
Label -> option (Just '.') '.' False Nothing Unreserved
PathSegment -> option (Just '/') '/' False Nothing Unreserved
PathParameter -> option (Just ';') ';' True Nothing Unreserved
Query -> option (Just '?') '&' True (Just '=') Unreserved
QueryContinuation -> option (Just '&') '&' True (Just '=') Unreserved
Fragment -> option (Just '#') ',' False Nothing UnreservedOrReserved
templateValueIsEmpty :: TemplateValue a -> Bool
templateValueIsEmpty (Single s) = T.null $ toUrlPiece s
templateValueIsEmpty (Associative s) = null s
templateValueIsEmpty (List s) = null s
{-# INLINE templateValueIsEmpty #-}
namePrefix ::
forall str a.
(Buildable str) =>
Proxy str ->
ProcessingOptions ->
T.Text ->
TemplateValue a ->
Builder str
namePrefix p opts name val =
addString p name
<> if templateValueIsEmpty val
then maybe mempty (addChar p) $ modifierIfEmpty opts
else addChar p '='
whenM :: Monoid m => Bool -> m -> m
whenM pred m = if pred then m else mempty
processVariable ::
forall str.
(Buildable str) =>
Proxy str ->
Modifier ->
Bool ->
Variable ->
WrappedValue ->
Builder str
processVariable p m isFirst (Variable varName varMod) (WrappedValue val) =
let prefix = maybe mempty (addChar p) $ modifierPrefix settings
separator = addChar p $ modifierSeparator settings
rest = case varMod of
Normal -> do
whenM
(modifierSupportsNamed settings)
(namePrefix p settings varName val)
<> unexploded
Explode -> exploded
MaxLength l ->
whenM
(modifierSupportsNamed settings)
(namePrefix p settings varName val)
<> unexploded
in (if isFirst then prefix else separator) <> rest
where
settings = options m
addEncodeString :: ToHttpApiData a => a -> Builder str
addEncodeString = allowEncoder (Proxy @str) (modifierAllow settings)
sepByCommas = mconcat . intersperse (addChar p ',')
associativeCommas :: (TemplateValue Single, TemplateValue Single) -> Builder str
associativeCommas (Single n, Single v) =
addEncodeString n
<> addChar p ','
<> addEncodeString (preprocess v)
preprocess :: ToHttpApiData a => a -> T.Text
preprocess = case varMod of
MaxLength l -> T.take l . toUrlPiece
_ -> toUrlPiece
unexploded :: Builder str
unexploded = case val of
(Associative l) -> sepByCommas $ map associativeCommas l
(List l) -> sepByCommas $ map (\(Single s) -> addEncodeString $ preprocess s) l
(Single s) -> addEncodeString $ preprocess s
explodedAssociative :: (TemplateValue Single, TemplateValue Single) -> Builder str
explodedAssociative (Single k, Single v) =
addEncodeString k
<> addChar p '='
<> addEncodeString (preprocess v)
exploded :: Builder str
exploded = case val of
(Single s) ->
whenM
(modifierSupportsNamed settings)
(namePrefix p settings varName val)
<> addEncodeString (preprocess s)
(Associative l) ->
mconcat $
intersperse (addChar p $ modifierSeparator settings) $
map explodedAssociative l
(List l) ->
mconcat $
intersperse (addChar p $ modifierSeparator settings) $
map
( \(Single s) ->
whenM
(modifierSupportsNamed settings)
(namePrefix p settings varName val)
<> addEncodeString (preprocess s)
)
l
processVariables ::
forall str.
(Buildable str) =>
Proxy str ->
[(T.Text, WrappedValue)] ->
Modifier ->
[Variable] ->
Builder str
processVariables p env m vs = mconcat processedVariables
where
findValue (Variable varName _) = lookup varName env
nonEmptyVariables :: [(Variable, WrappedValue)]
nonEmptyVariables = mapMaybe (\v -> (\mv -> (v, mv)) <$> findValue v) vs
processors :: [Variable -> WrappedValue -> Builder str]
processors = processVariable p m True : repeat (processVariable p m False)
processedVariables :: [Builder str]
processedVariables = zipWith uncurry processors nonEmptyVariables
render :: (Buildable str) => UriTemplate -> [BoundValue] -> str
render = render'
render' :: forall str. (Buildable str) => UriTemplate -> [BoundValue] -> str
render' (UriTemplate tpl) env = build $ V.foldMap go tpl
where
p :: Proxy str
p = Proxy
go :: TemplateSegment -> Builder str
go (Literal s) = addString p s
go (Embed m vs) = processVariables p env m vs