packages feed

buffer-builder-0.2.1.0: bench/UrlBench.hs

{-# LANGUAGE RecordWildCards, OverloadedStrings #-}

module Main (main) where

import qualified Data.BufferBuilder as BB
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Builder as BSB
import qualified Data.ByteString.Char8 as BSC
import Criterion
import Criterion.Main
import Data.Word (Word16)
import Data.Monoid
import Control.Monad
import Network.HTTP.Types (urlEncode)

type Protocol = BS.ByteString
type PathPiece = BS.ByteString

type Hash = BS.ByteString

data SMaybe a = SNothing | SJust !a
    deriving (Eq, Show, Ord) 

data UrlHost = UrlHost
    { uhHostName :: !BS.ByteString
    , uhPort     :: {-# UNPACK #-} !Word16         -- 0 = no port
    , uhUserName :: !BS.ByteString  -- empty = no username
    , uhPasswd   :: !BS.ByteString  -- empty = no password
    } deriving (Show, Ord, Eq)

{- | The type of a URL's query arguments.
   The RFC does not specify any particular format for this section of a URL, but, since
   it is so common to use form encoding, we include a representation which is a list of
   parsed key=value&key=value pairs.

   When parsing a URL, we first assume that the query is form encoded.  If this parse fails,
   the 'UnparsedQuery' constructor is instead used to encode a URL whose query encoding is
   unknown.
-}
data QueryPair = QueryPair !BS.ByteString !BS.ByteString
     deriving (Show, Ord, Eq)
data Query
    = UnparsedQuery !BS.ByteString
    | ParsedQuery [QueryPair]
      deriving (Show, Ord, Eq)
               
{- | Symbolic URL type.
   It is intended that this type can capture all \"conventional\" URLs losslessly and symbolically.

   In short, if you ever find yourself resorting to direct string parsing or generation, you are
   either doing it wrong, or this library is missing something you need.
 -}

data UrlHead = FullyQualified !Protocol !UrlHost
             | ProtocolRelative !UrlHost
             | Absolute
             | Relative
             deriving (Eq, Ord, Show)

data Url = Url
    { urlHead     :: !UrlHead
    , urlPath     :: ![PathPiece]
    , urlQuery    :: !(SMaybe Query)
    , urlHash     :: !(SMaybe Hash)
    } deriving (Eq, Ord, Show)


builderToBS :: BSB.Builder -> BS.ByteString
builderToBS = BSL.toStrict . BSB.toLazyByteString

escapeString :: BS.ByteString -> BSB.Builder
escapeString = BSB.byteString . urlEncode False

toByteString :: Url -> BS.ByteString
toByteString = builderToBS . toBuilder

toBuilder :: Url -> BSB.Builder
toBuilder Url{..} =
    renderHead urlHead
    <> renderPath urlPath
    <> renderQuery urlQuery
    <> renderHash urlHash
  where
    toBB = BSB.byteString
    char = BSB.char7

    renderHead :: UrlHead -> BSB.Builder
    renderHead (FullyQualified protocol host) = toBB protocol <> char ':' <> renderHost host <> char '/'
    renderHead (ProtocolRelative host) = renderHost host <> char '/'
    renderHead Absolute = char '/'
    renderHead Relative = mempty

    renderHost :: UrlHost -> BSB.Builder
    renderHost UrlHost{..} = char '/' <> char '/' <> renderUser uhUserName uhPasswd <> renderHostName uhHostName uhPort

    renderPath :: [PathPiece] -> BSB.Builder
    renderPath [] = mempty
    renderPath path = interc (char '/') (map escapeString path)

    renderQuery :: SMaybe Query -> BSB.Builder
    renderQuery SNothing = mempty
    renderQuery (SJust (UnparsedQuery q)) = char '?' <> toBB q
    renderQuery (SJust (ParsedQuery pairs)) = char '?' <> interc (char '&') (map renderPair pairs)

    renderPair (QueryPair k v) = escapeString k <> char '=' <> escapeString v

    renderHash h = case h of
        SNothing -> mempty
        SJust hash -> char '#' <> toBB hash

    renderHostName hostName hnPort
        | 0 /= hnPort = toBB hostName <> char ':' <> BSB.intDec (fromIntegral hnPort)
        | otherwise   = toBB hostName

    renderUser uhName uhPasswd
        | BS.null uhName = mempty
        | BS.null uhPasswd = escapeString uhName <> char '@'
        | otherwise = escapeString uhName <> char ':' <> escapeString uhPasswd <> char '@'

    interc _ [] = mempty
    interc _ [x] = x
    interc sep (x:y:xs) = x <> sep <> interc sep (y:xs)

renderHost' :: UrlHost -> BB.BufferBuilder ()
renderHost' UrlHost{..} = do
    BB.appendChar8 '/'
    BB.appendChar8 '/'

    when (not $ BS.null uhUserName) $ do
        BB.appendUrlEncoded uhUserName
        when (not $ BS.null uhPasswd) $ do
            BB.appendChar8 ':'
            BB.appendUrlEncoded uhPasswd
        BB.appendChar8 '@'

    BB.appendBS uhHostName
    if (uhPort /= 0) then do
        BB.appendChar8 ':'
        BB.appendDecimalSignedInt (fromIntegral uhPort)
    else
        return ()
        
    BB.appendChar8 '/'

renderPair' :: QueryPair -> BB.BufferBuilder ()
renderPair' (QueryPair key value) = do
    BB.appendUrlEncoded key
    BB.appendChar8 '='
    BB.appendUrlEncoded value
    
render :: Url -> BB.BufferBuilder ()
render Url{..} = do
    case urlHead of
        (FullyQualified protocol host) -> do
            BB.appendBS protocol
            BB.appendChar8 ':'
            renderHost' host
        (ProtocolRelative host) -> renderHost' host
        Absolute -> BB.appendChar8 '/'
        Relative -> return ()

    case urlPath of
        [] -> return ()
        (x:xs) -> do
            BB.appendUrlEncoded x
            forM_ xs $ \ps -> do
                BB.appendChar8 '/'
                BB.appendUrlEncoded ps

    case urlQuery of
        SNothing -> return ()
        (SJust (UnparsedQuery q)) -> do
            BB.appendChar8 '?'
            BB.appendBS q
        (SJust (ParsedQuery pairs)) -> do
            BB.appendChar8 '?'
            case pairs of
                [] -> return ()
                (x:xs) -> do
                    renderPair' x
                    forM_ xs $ \pair -> do
                        BB.appendChar8 '&'
                        renderPair' pair


    case urlHash of
        SNothing -> return ()
        (SJust h) -> do
            BB.appendChar8 '#'
            BB.appendBS h

viaBufferBuilder :: Url -> BS.ByteString
viaBufferBuilder = BB.runBufferBuilder . render

main :: IO ()
main = do
    let host = UrlHost
            { uhHostName = "example.com"
            , uhPort = 80
            , uhUserName = ""
            , uhPasswd = ""
            }

    let url = Url
            { urlHead = FullyQualified "http" host
            , urlPath = ["service", "one", "two", "three"]
            , urlQuery = SJust (ParsedQuery
                                  [ QueryPair "limit" "30"
                                  , QueryPair "next" "123456"
                                  , QueryPair "previous" "987654"
                                  ])
            , urlHash = SJust "anchor"
            }

    BSC.putStrLn $ "bytestring builder: " <> toByteString url
    BSC.putStrLn $ "bufferbuilder: " <> viaBufferBuilder url

    defaultMain [ bgroup "render url"
                    [ bench "bufferbuilder" $ nf viaBufferBuilder url
                    , bench "bytestring builder" $ nf toByteString url
                    --, bench "vector text" $ nf Json.encodeJson (Vector.replicate 100000 ("hello world" :: Text))
                    ]
                ]