{-# LANGUAGE ExistentialQuantification , FlexibleInstances , MultiParamTypeClasses , OverlappingInstances , TemplateHaskell , TypeFamilies , UndecidableInstances , UnicodeSyntax , ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Network.HTTP.Lucu.StatusCode.Internal ( StatusCode(..) , SomeStatusCode , statusCodes ) where import Control.Applicative import Data.Ascii (Ascii, AsciiBuilder) import qualified Data.Ascii as A import Data.Attoparsec.Char8 import qualified Data.Attoparsec.Lazy as LP import qualified Data.ByteString.Lazy.Char8 as Lazy import Data.Convertible.Base import Data.Convertible.Instances.Ascii () import Data.Convertible.Utils import Data.List import Language.Haskell.TH.Lib import Language.Haskell.TH.Syntax import Language.Haskell.TH.Quote import Network.HTTP.Lucu.Parser import Prelude.Unicode -- |Type class for HTTP status codes. -- -- Declaring types for each statuses is surely a pain. See: -- 'statusCodes' quasi-quoter. -- -- Minimal complete definition: 'numericCode' and 'textualStatus'. class (Eq sc, Show sc) ⇒ StatusCode sc where -- |Return the 3-digit integer for this status e.g. @200@ numericCode ∷ sc → Int -- |Return the combination of 3-digit integer and reason phrase -- for this status e.g. @200 OK@ textualStatus ∷ sc → AsciiBuilder -- |Wrap the status code into 'SomeStatusCode'. fromStatusCode ∷ sc → SomeStatusCode fromStatusCode = SomeStatusCode instance StatusCode sc ⇒ ConvertSuccess sc SomeStatusCode where {-# INLINE convertSuccess #-} convertSuccess = fromStatusCode instance StatusCode sc ⇒ ConvertSuccess sc Ascii where {-# INLINE convertSuccess #-} convertSuccess = convertSuccessVia ((⊥) ∷ AsciiBuilder) instance StatusCode sc ⇒ ConvertSuccess sc AsciiBuilder where {-# INLINE convertSuccess #-} convertSuccess = textualStatus instance StatusCode sc ⇒ ConvertAttempt sc SomeStatusCode where {-# INLINE convertAttempt #-} convertAttempt = return ∘ cs instance StatusCode sc ⇒ ConvertAttempt sc Ascii where {-# INLINE convertAttempt #-} convertAttempt = return ∘ cs instance StatusCode sc ⇒ ConvertAttempt sc AsciiBuilder where {-# INLINE convertAttempt #-} convertAttempt = return ∘ cs -- |Container type for the 'StatusCode' type class. data SomeStatusCode = ∀sc. StatusCode sc ⇒ SomeStatusCode sc -- |Equivalence of 'StatusCode's. Two 'StatusCode's @α@ and -- @β@ are said to be equivalent iff @'numericCode' α '==' -- 'numericCode' β@. instance Eq SomeStatusCode where {-# INLINE (==) #-} (==) = (∘ numericCode) ∘ (==) ∘ numericCode instance Show SomeStatusCode where show (SomeStatusCode sc) = show sc instance StatusCode SomeStatusCode where numericCode (SomeStatusCode sc) = numericCode sc textualStatus (SomeStatusCode sc) = textualStatus sc fromStatusCode = id -- |'QuasiQuoter' for 'StatusCode' declarations. -- -- Top-level splicing -- -- @ -- ['statusCodes'| -- 200 OK -- 400 Bad Request -- 405 Method Not Allowed -- |] -- @ -- -- becomes: -- -- @ -- data OK = OK deriving ('Eq', 'Show') -- instance OK where -- 'numericCode' _ = 200 -- 'textualStatus' _ = 'cs' (\"200 OK\" ∷ Ascii) -- -- data BadRequest = BadRequest deriving ('Eq', 'Show') -- instance BadRequest where -- 'numericCode' _ = 400 -- 'textualStatus' _ = 'cs' (\"400 Bad Request\" ∷ Ascii) -- -- data MethodNotAllowed = MethodNotAllowed deriving ('Eq', 'Show') -- instance MethodNotAllowed where -- 'numericCode' _ = 405 -- 'textualStatus' _ = 'cs' (\"405 Method Not Allowed\" ∷ Ascii) -- @ statusCodes ∷ QuasiQuoter statusCodes = QuasiQuoter { quoteExp = const unsupported , quotePat = const unsupported , quoteType = const unsupported , quoteDec = (concat <$>) ∘ mapM statusDecl ∘ parseStatusCodes ∘ Lazy.pack } where unsupported ∷ Monad m ⇒ m α unsupported = fail "Unsupported usage of statusCodes quasi-quoter." parseStatusCodes ∷ Lazy.ByteString → [(Int, [Ascii])] parseStatusCodes src = case LP.parse pairs src of LP.Fail _ eCtx e → error $ "Unparsable status codes: " ⧺ intercalate ", " eCtx ⧺ ": " ⧺ e LP.Done _ xs → xs where pairs ∷ Parser [(Int, [Ascii])] pairs = do skipMany endOfLine xs ← sepBy pair (skipMany1 endOfLine) skipMany endOfLine endOfInput return xs "pairs" pair ∷ Parser (Int, [Ascii]) pair = do skipSpace num ← decimal skipSpace1 phrase ← sepBy1 word $ skipWhile1 (≡ '\x20') return (num, phrase) "pair" word ∷ Parser Ascii word = A.unsafeFromByteString <$> takeWhile1 isAlpha_ascii statusDecl ∷ (Int, [Ascii]) → Q [Dec] statusDecl (num, phrase) = do a ← dataDecl bs ← instanceDecl return (a:bs) where name ∷ Name name = mkName $ concatMap cs phrase dataDecl ∷ Q Dec dataDecl = dataD (cxt []) name [] [con] [''Eq, ''Show] instanceDecl ∷ Q [Dec] instanceDecl = [d| instance StatusCode $typ where {-# INLINE CONLIKE numericCode #-} numericCode _ = $(lift num) {-# INLINE CONLIKE textualStatus #-} textualStatus _ = $txt |] typ ∷ Q Type typ = conT name con ∷ Q Con con = return $ NormalC name [] txt ∷ Q Exp txt = [| cs ($(lift txt') ∷ Ascii) |] txt' ∷ String txt' = concat $ intersperse "\x20" $ show num : map cs phrase