]> gitweb @ CieloNegro.org - Lucu.git/blobdiff - Network/HTTP/Lucu/MIMEType/Guess.hs
DefaultExtensionMap is now generated with TH.
[Lucu.git] / Network / HTTP / Lucu / MIMEType / Guess.hs
index 7fe58206b5dcb86cd15538c4906f2f86ed0a09d8..8cddcba19bd60934b934f07dd520c97ef9c7fad9 100644 (file)
+{-# LANGUAGE
+    DeriveDataTypeable
+  , GeneralizedNewtypeDeriving
+  , TemplateHaskell
+  , UnicodeSyntax
+  , ViewPatterns
+  #-}
+-- |Guessing MIME Types by file extensions. It's not always accurate
+-- but simple and fast.
+--
+-- In general you don't have to use this module directly.
 module Network.HTTP.Lucu.MIMEType.Guess
-    ( ExtMap
-    , guessTypeByFileName -- ExtMap -> FilePath -> Maybe MIMEType
-
-    , parseExtMapFile  -- FilePath -> IO ExtMap
-    , outputExtMapAsHS -- ExtMap -> FilePath -> IO ()
+    ( ExtMap(..)
+    , extMap
+    , parseExtMap
+    , guessTypeByFileName
     )
     where
-
-import qualified Data.ByteString.Lazy.Char8 as B
-import           Data.ByteString.Lazy.Char8 (ByteString)
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Unicode
+import Data.Ascii (Ascii)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8 as P
 import qualified Data.Map as M
-import           Data.Map (Map)
-import           Data.Maybe
-import           Language.Haskell.Pretty
-import           Language.Haskell.Syntax
-import           Network.HTTP.Lucu.MIMEType
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-import           Network.HTTP.Lucu.Utils
-import           System.IO
-
-type ExtMap = Map String MIMEType
-
-
-guessTypeByFileName :: ExtMap -> FilePath -> Maybe MIMEType
-guessTypeByFileName extMap fpath
-    = let ext = head $ reverse $ splitBy (== '.') fpath
-      in
-        M.lookup ext extMap >>= return
-
-
-parseExtMapFile :: FilePath -> IO ExtMap
-parseExtMapFile fpath
-    = do file <- B.readFile fpath
-         case parse (allowEOF extMapP) file of
-           (Success xs, _) -> return $ compile xs
-           (_, input')     -> let near = B.unpack $ B.take 100 input'
-                              in 
-                                fail ("Failed to parse: " ++ fpath ++ " (near: " ++ near ++ ")")
-
-
-extMapP :: Parser [ (MIMEType, [String]) ]
-extMapP = do xs <- many (comment <|> validLine <|> emptyLine)
-             eof
-             return $ catMaybes xs
+import Data.Map (Map)
+import Data.Maybe
+import Data.Typeable
+import Data.Monoid
+import Data.Monoid.Unicode
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Encoding
+import Language.Haskell.TH.Syntax
+import Language.Haskell.TH.Quote
+import Network.HTTP.Lucu.MIMEType
+import Network.HTTP.Lucu.Parser
+import Network.HTTP.Lucu.Utils
+import Prelude.Unicode
+import System.FilePath
+
+-- |A 'Map' from file extensions to 'MIMEType's.
+newtype ExtMap
+    = ExtMap (Map Text MIMEType)
+    deriving (Eq, Show, Read, Monoid, Typeable)
+
+instance Lift ExtMap where
+    lift (ExtMap m)
+        = [| ExtMap $(liftMap liftText lift m) |]
+
+-- |'QuasiQuoter' for 'ExtMap' reading Apache @mime.types@.
+--
+-- @
+--   m :: 'ExtMap'
+--   m = ['extMap'|
+--   # MIME Type            Extensions
+--   application/xhtml+xml  xhtml
+--   image/jpeg             jpeg jpg
+--   image/png              png
+--   image/svg+xml          svg
+--   text/html              html
+--   text/plain             txt
+--   |]
+-- @
+extMap ∷ QuasiQuoter
+extMap = QuasiQuoter {
+             quoteExp  = (lift ∘ parseExtMap =≪) ∘ toAscii
+           , quotePat  = const unsupported
+           , quoteType = const unsupported
+           , quoteDec  = const unsupported
+         }
+    where
+      toAscii ∷ Monad m ⇒ String → m Ascii
+      toAscii (A.fromChars → Just a) = return a
+      toAscii _ = fail "Malformed extension map"
+
+      unsupported ∷ Monad m ⇒ m α
+      unsupported = fail "Unsupported usage of extMap quasi-quoter."
+
+-- |Parse Apache @mime.types@.
+parseExtMap ∷ Ascii → ExtMap
+parseExtMap src
+    = case parseOnly (finishOff extMapP) $ A.toByteString src of
+        Right xs → case compile xs of
+                      Right m → ExtMap m
+                      Left  e → error ("Duplicate extension: " ⧺ show e)
+        Left err → error ("Unparsable extension map: " ⧺ err)
+
+extMapP ∷ Parser [(MIMEType, [Text])]
+extMapP = catMaybes <$> P.many (try comment <|> try validLine <|> emptyLine)
     where
-      spc = oneOf " \t"
+      isSpc ∷ Char → Bool
+      isSpc c = c ≡ '\x20' ∨ c ≡ '\x09'
 
-      comment = do many spc
-                   char '#'
-                   many $ satisfy (/= '\n')
+      comment ∷ Parser (Maybe (MIMEType, [Text]))
+      comment = do skipWhile isSpc
+                   void $ char '#'
+                   skipWhile (≢ '\x0A')
                    return Nothing
 
-      validLine = do many spc
-                     mime <- mimeTypeP
-                     many spc
-                     exts <- sepBy token (many spc)
+      validLine ∷ Parser (Maybe (MIMEType, [Text]))
+      validLine = do skipWhile isSpc
+                     mime ← mimeType
+                     skipWhile isSpc
+                     exts ← sepBy extP (skipWhile isSpc)
                      return $ Just (mime, exts)
 
-      emptyLine = oneOf " \t\n" >> return Nothing
-
-
-compile :: [ (MIMEType, [String]) ] -> Map String MIMEType
-compile = M.fromList . foldr (++) [] . map tr
-    where
-      tr :: (MIMEType, [String]) -> [ (String, MIMEType) ]
-      tr (mime, exts) = [ (ext, mime) | ext <- exts ]
+      extP ∷ Parser Text
+      extP = decodeUtf8 <$> takeWhile1 (\c → (¬) (isSpc c ∨ c ≡ '\x0A'))
 
+      emptyLine ∷ Parser (Maybe (MIMEType, [Text]))
+      emptyLine = do skipWhile isSpc
+                     void $ char '\x0A'
+                     return Nothing
 
-outputExtMapAsHS :: ExtMap -> FilePath -> IO ()
-outputExtMapAsHS extMap fpath
-    = let hsModule = HsModule undefined modName (Just exports) imports decls
-          modName  = Module "Network.HTTP.Lucu.MIMEType.DefaultExtensionMap"
-          exports  = [HsEVar (UnQual (HsIdent "defaultExtensionMap"))]
-          imports  = [ HsImportDecl undefined (Module "Network.HTTP.Lucu.MIMEType") False Nothing Nothing
-                     , HsImportDecl undefined (Module "Data.Map") True (Just (Module "M")) Nothing
-                     , HsImportDecl undefined (Module "Data.Map") False Nothing (Just (False, [HsIAbs (HsIdent "Map")]))
-                     ]
-          decls    = [ HsTypeSig undefined [HsIdent "defaultExtensionMap"]
-                                     (HsQualType [] (HsTyApp (HsTyApp (HsTyCon (UnQual (HsIdent "Map")))
-                                                                      (HsTyCon (UnQual (HsIdent "String"))))
-                                                             (HsTyCon (UnQual (HsIdent "MIMEType")))))
-                     , HsFunBind [HsMatch undefined (HsIdent "defaultExtensionMap")
-                                  [] (HsUnGuardedRhs extMapExp) []]
-                     ]
-          extMapExp = HsApp (HsVar (Qual (Module "M") (HsIdent "fromList"))) (HsList records)
-          comment =    "{- !!! WARNING !!!\n"
-                    ++ "   This file is automatically generated from data/mime.types.\n"
-                    ++ "   DO NOT EDIT BY HAND OR YOU WILL REGRET -}\n\n"
-      in
-        writeFile fpath $ comment ++ prettyPrint hsModule ++ "\n"
+compile ∷ Ord k ⇒ [(v, [k])] → Either (k, v, v) (Map k v)
+compile = go (∅) ∘ concat ∘ map tr
     where
-      records :: [HsExp]
-      records = map record $ M.assocs extMap
-
-      record :: (String, MIMEType) -> HsExp
-      record (ext, mime)
-          = HsTuple [HsLit (HsString ext), mimeToExp mime]
-                    
-      mimeToExp :: MIMEType -> HsExp
-      mimeToExp (MIMEType maj min params)
-          = foldl appendParam (HsInfixApp
-                               (HsLit (HsString maj))
-                               (HsQVarOp (UnQual (HsSymbol "+/+")))
-                               (HsLit (HsString min))) params
-
-      appendParam :: HsExp -> (String, String) -> HsExp
-      appendParam x param
-          = HsInfixApp x (HsQVarOp (UnQual (HsSymbol "+:+"))) $ paramToExp param
-
-      paramToExp :: (String, String) -> HsExp
-      paramToExp (name, value)
-          = HsInfixApp
-            (HsLit (HsString name))
-            (HsQVarOp (UnQual (HsSymbol "+=+")))
-            (HsLit (HsString value))
\ No newline at end of file
+      tr ∷ (v, [k]) → [(k, v)]
+      tr (v, ks) = [(k, v) | k ← ks]
+
+      go ∷ Ord k ⇒ Map k v → [(k, v)] → Either (k, v, v) (Map k v)
+      go m []         = Right m
+      go m ((k, v):xs)
+          = case M.insertLookupWithKey' f k v m of
+              (Nothing, m') → go m' xs
+              (Just v0, _ ) → Left (k, v0, v)
+
+      f ∷ k → v → v → v
+      f _ _ = id
+
+-- |Guess the MIME Type of a file.
+guessTypeByFileName ∷ ExtMap → FilePath → Maybe MIMEType
+guessTypeByFileName (ExtMap m) fpath
+    = case takeExtension fpath of
+        []      → Nothing
+        (_:ext) → M.lookup (T.pack ext) m