{-# LANGUAGE
- UnicodeSyntax
+ OverloadedStrings
+ , UnicodeSyntax
#-}
module Main where
import Codec.Compression.GZip
import Control.Applicative
import Control.Monad
import qualified Data.Ascii as A
-import Data.Bits
import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Char8 as Strict
import qualified Data.ByteString.Lazy as Lazy
import Data.Char
+import Data.Digest.Pure.SHA
import Data.Int
import Data.Maybe
import Data.Time
exitWith $ ExitFailure 1
when (length sources ≥ 2)
- $ error "too many input files."
+ $ fail "too many input files."
generateHaskellSource opts (head sources)
header ← mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
- let hsModule = mkModule (ModuleName modName) (name symName) imports decls
+ let hsModule = mkModule modName symName imports decls
imports = mkImports useGZip
- decls = concat [ declResourceDef
+ decls = concat [ resourceDecl symName useGZip
, entityTagDecl eTag
, lastModifiedDecl lastMod
, contentTypeDecl mimeType
else
dataDecl (name "rawData") rawB64
]
- declResourceDef
- = [ HsTypeSig (⊥) [HsIdent symName]
- (HsQualType []
- (HsTyCon (UnQual (HsIdent "ResourceDef"))))
- , HsFunBind [HsMatch (⊥) (HsIdent symName)
- [] (HsUnGuardedRhs defResourceDef) []]
- ]
-
- defResourceDef ∷ HsExp
- defResourceDef
- = let defResGet = if useGZip
- then defResGetGZipped
- else resGetRaw
- in
- (HsRecConstr (UnQual (HsIdent "ResourceDef"))
- [ HsFieldUpdate (UnQual (HsIdent "resUsesNativeThread"))
- (HsCon (UnQual (HsIdent "False")))
- , HsFieldUpdate (UnQual (HsIdent "resIsGreedy"))
- (HsCon (UnQual (HsIdent "False")))
- , HsFieldUpdate (UnQual (HsIdent "resGet")) defResGet
- , HsFieldUpdate (UnQual (HsIdent "resHead"))
- (HsCon (UnQual (HsIdent "Nothing")))
- , HsFieldUpdate (UnQual (HsIdent "resPost"))
- (HsCon (UnQual (HsIdent "Nothing")))
- , HsFieldUpdate (UnQual (HsIdent "resPut"))
- (HsCon (UnQual (HsIdent "Nothing")))
- , HsFieldUpdate (UnQual (HsIdent "resDelete"))
- (HsCon (UnQual (HsIdent "Nothing")))
- ]
- )
-
- defResGetGZipped ∷ HsExp
- defResGetGZipped
- = let doExp = HsDo [ foundEntityStmt
- , setContentTypeStmt
- , bindMustGunzip
- , doConditionalOutput
- ]
- bindMustGunzip
- = HsGenerator (⊥)
- (HsPVar (HsIdent "mustGunzip"))
- (HsApp (HsApp (HsVar (UnQual (HsIdent "liftM")))
- (HsVar (UnQual (HsIdent "not"))))
- (HsParen
- (HsApp (HsVar (UnQual (HsIdent "isEncodingAcceptable")))
- (HsLit (HsString "gzip")))))
- doConditionalOutput
- = HsQualifier
- (HsIf (HsVar (UnQual (HsIdent "mustGunzip")))
- expOutputGunzipped
- expOutputGZipped)
- expOutputGunzipped
- = (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
- (HsParen
- (HsApp (HsVar (UnQual (HsIdent "decompress")))
- (HsVar (UnQual (HsIdent "gzippedData"))))))
- expOutputGZipped
- = HsDo [ setContentEncodingGZipStmt
- , outputStmt (var $ name "gzippedData")
- ]
- in
- HsApp (HsCon (UnQual (HsIdent "Just")))
- (HsParen doExp)
hPutStrLn output header
hPutStrLn output (prettyPrint hsModule)
hClose output
-mkModule ∷ ModuleName → Name → [ImportDecl] → [Decl]
+mkModule ∷ ModuleName → Name → [ImportDecl] → [Decl] → Module
mkModule modName symName imports decls
- = let modPragma = [ LanguagePragma (⊥) [ name (show OverloadedStrings)
- ]
- ]
- exports = [ EVar (UnQual symName)
- ]
+ = let modPragma = [ LanguagePragma (⊥) [ name (show OverloadedStrings) ] ]
+ exports = [ EVar (UnQual symName) ]
in
Module (⊥) modName modPragma Nothing (Just exports) imports decls
mkImports ∷ Bool → [ImportDecl]
mkImports useGZip
= [ ImportDecl (⊥) (ModuleName "Data.ByteString.Base64")
- True False (Just (ModuleName "B64")) Nothing
+ True False Nothing (Just (ModuleName "B64")) Nothing
, ImportDecl (⊥) (ModuleName "Data.ByteString.Lazy")
- True False (Just (ModuleName "Lazy")) Nothing
+ True False Nothing (Just (ModuleName "Lazy")) Nothing
, ImportDecl (⊥) (ModuleName "Data.Time")
- False False Nothing Nothing
+ False False Nothing Nothing Nothing
, ImportDecl (⊥) (ModuleName "Network.HTTP.Lucu")
- False False Nothing Nothing
+ False False Nothing Nothing Nothing
]
⧺
if useGZip then
[ ImportDecl (⊥) (ModuleName "Codec.Compression.GZip")
- False False Nothing Nothing
+ False False Nothing Nothing Nothing
]
else
[]
+resourceDecl ∷ Name → Bool → [Decl]
+resourceDecl symName useGZip
+ = [ TypeSig (⊥) [symName] (TyCon (UnQual (name "ResourceDef")))
+ , nameBind (⊥) symName valExp
+ ]
+ where
+ valExp ∷ Exp
+ valExp = RecUpdate (var $ name "emptyResource")
+ [ FieldUpdate (UnQual (name "resGet" )) resGet
+ , FieldUpdate (UnQual (name "resHead")) resHead
+ ]
+
+ resGet ∷ Exp
+ resGet | useGZip = resGetGZipped
+ | otherwise = resGetRaw
+
resHead ∷ Exp
resHead
= infixApp (var $ name "Just")
, setContentTypeStmt
])
+resGetGZipped ∷ Exp
+resGetGZipped
+ = infixApp (var $ name "Just")
+ (op $ name "$" )
+ (doE [ foundEntityStmt
+ , setContentTypeStmt
+ , bindGZipStmt
+ , conditionalOutputStmt
+ ])
+ where
+ condVarName ∷ Name
+ condVarName = name "gzipAllowed"
+
+ dataVarName ∷ Name
+ dataVarName = name "gzippedData"
+
+ bindGZipStmt ∷ Stmt
+ bindGZipStmt
+ = genStmt (⊥)
+ (pvar condVarName)
+ (metaFunction "isEncodingAcceptable" [strE "gzip"])
+
+ conditionalOutputStmt ∷ Stmt
+ conditionalOutputStmt
+ = qualStmt $
+ If (var condVarName)
+ (doE [ setContentEncodingGZipStmt
+ , outputStmt (var dataVarName)
+ ])
+ (metaFunction "output"
+ [paren (metaFunction "decompress" [var dataVarName])])
+
resGetRaw ∷ Exp
resGetRaw
= infixApp (var $ name "Just")
valExp ∷ Exp
valExp = metaFunction "parseETag" [strE $ eTagToString eTag]
- eTagToString ∷ ETag → String
- eTagToString = A.toString ∘ A.fromAsciiBuilder ∘ printETag
-
lastModifiedDecl ∷ UTCTime → [Decl]
lastModifiedDecl lastMod
= [ TypeSig (⊥) [varName] (TyCon (UnQual (name "UTCTime")))
varName = name "contentType"
valExp ∷ Exp
- valExp = metaFunction "parseMIMEType" [mimeToString mime]
+ valExp = metaFunction "parseMIMEType" [strE $ mimeToString mime]
mimeToString ∷ MIMEType → String
mimeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
`app`
strE (Strict.unpack chunk)
-mkHeader ∷ FilePath → Int64 → Int64 → Bool → MIMEType → String → UTCTime → IO String
+mkHeader ∷ FilePath → Int64 → Int64 → Bool → MIMEType → ETag → UTCTime → IO String
mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
= do localLastMod ← utcToLocalZonedTime lastMod
- return ("{- DO NOT EDIT THIS FILE.\n" ++
- " This file is automatically generated by the lucu-implant-file program.\n" ++
- "\n" ++
- " Source: " ++ (if srcFile ≡ "-"
- then "(stdin)"
- else srcFile) ++ "\n" ++
- " Original Length: " ++ show originalLen ++ " bytes\n" ++
- (if useGZip
- then " Compressed Length: " ++ show gzippedLen ++ " bytes\n" ++
- " Compression: gzip\n"
- else " Compression: disabled\n") ++
- " MIME Type: " ++ show mimeType ++ "\n" ++
- " ETag: " ++ eTag ++ "\n" ++
- " Last Modified: " ++ show localLastMod ++ "\n" ++
- " -}")
-
-
-getModuleName ∷ [CmdOpt] → IO String
+ return $ concat
+ [ "{- DO NOT EDIT THIS FILE.\n"
+ , " This file is automatically generated by the lucu-implant-file program.\n"
+ , "\n"
+ , " Source: ", if srcFile ≡ "-" then
+ "(stdin)"
+ else
+ srcFile
+ , "\n"
+ , " Original Length: ", show originalLen, " bytes\n"
+ , if useGZip then
+ " Compressed Length: " ⧺ show gzippedLen ⧺ " bytes\n" ⧺
+ " Compression: gzip\n"
+ else
+ " Compression: disabled\n"
+ , " MIME Type: ", show mimeType, "\n"
+ , " ETag: ", eTagToString eTag, "\n"
+ , " Last Modified: ", show localLastMod, "\n"
+ , " -}"
+ ]
+
+eTagToString ∷ ETag → String
+eTagToString = A.toString ∘ A.fromAsciiBuilder ∘ printETag
+
+getModuleName ∷ [CmdOpt] → IO ModuleName
getModuleName opts
- = let modNameOpts = filter (\ x → case x of
- OptModName _ → True
- _ → False) opts
- in
- case modNameOpts of
- [] → error "a module name must be given."
- (OptModName modName):[] → return modName
- _ → error "too many --module options."
-
-
-getSymbolName ∷ [CmdOpt] → String → IO String
-getSymbolName opts modName
- = let symNameOpts = filter (\ x → case x of
- OptSymName _ → True
- _ → False) opts
- -- モジュール名をピリオドで分割した時の最後の項目の先頭文字を
- -- 小文字にしたものを使ふ。
- defaultSymName = mkDefault modName
- mkDefault = headToLower ∘ getLastComp
- headToLower str = case str of
- [] → error "module name must not be empty"
- (x:xs) → toLower x : xs
- getLastComp = reverse ∘ fst ∘ break (≡ '.') ∘ reverse
- in
- case symNameOpts of
- [] → return defaultSymName
- (OptSymName symName):[] → return symName
- _ → error "too many --symbol options."
+ = case modNameOpts of
+ [] → fail "a module name must be given."
+ (OptModName modName):[] → return $ ModuleName modName
+ _ → fail "too many --module options."
+ where
+ modNameOpts ∷ [CmdOpt]
+ modNameOpts = filter (\ x → case x of
+ OptModName _ → True
+ _ → False) opts
+
+getSymbolName ∷ [CmdOpt] → ModuleName → IO Name
+getSymbolName opts (ModuleName modName)
+ = case symNameOpts of
+ [] → return defaultSymName
+ (OptSymName symName):[] → return $ name symName
+ _ → fail "too many --symbol options."
+ where
+ symNameOpts ∷ [CmdOpt]
+ symNameOpts = filter (\ x → case x of
+ OptSymName _ → True
+ _ → False) opts
+
+ defaultSymName ∷ Name
+ defaultSymName
+ = name $ headToLower $ getLastComp modName
+ headToLower ∷ String → String
+ headToLower [] = error "module name must not be empty"
+ headToLower (x:xs) = toLower x : xs
+
+ getLastComp ∷ String → String
+ getLastComp = reverse ∘ fst ∘ break (≡ '.') ∘ reverse
getMIMEType ∷ [CmdOpt] → FilePath → IO MIMEType
getMIMEType opts srcFile
= case mimeTypeOpts of
- [] → return defaultType
- (OptMIMEType ty):[] → return $ read ty
- _ → error "too many --mime-type options."
+ [] → return defaultType
+ (OptMIMEType ty):[]
+ → case A.fromChars ty of
+ Just a → return $ parseMIMEType a
+ Nothing → fail "MIME type must not contain any non-ASCII letters."
+ _ → fail "too many --mime-type options."
where
mimeTypeOpts ∷ [CmdOpt]
mimeTypeOpts
defaultType = fromMaybe octetStream
$ guessTypeByFileName defaultExtensionMap srcFile
-
getLastModified ∷ FilePath → IO UTCTime
getLastModified "-" = getCurrentTime
getLastModified fpath = (posixSecondsToUTCTime ∘ fromRational ∘ toRational ∘ modificationTime)
<$>
getFileStatus fpath
-
-getETag ∷ [CmdOpt] → Lazy.ByteString → IO String
+getETag ∷ [CmdOpt] → Lazy.ByteString → IO ETag
getETag opts input
- = let eTagOpts = filter (\ x → case x of
- OptETag _ → True
- _ → False) opts
- in
- case eTagOpts of
- [] → fmap (mkETagFromInput ∘ fromJust) (getDigestByName "SHA1")
- (OptETag str):[] → return str
- _ → error "too many --etag options."
+ = case eTagOpts of
+ [] → return $ mkETagFromInput
+ (OptETag str):[] → return $ strToETag str
+ _ → fail "too many --etag options."
where
- mkETagFromInput ∷ Digest → String
- mkETagFromInput sha1 = "SHA-1:" ++ toHex (digestLBS sha1 input)
-
- toHex ∷ String → String
- toHex = foldr ((++) ∘ hexByte ∘ fromEnum) ""
-
- hexByte ∷ Int → String
- hexByte n
- = [ hex4bit ((n `shiftR` 4) .&. 0x0F)
- , hex4bit ( n .&. 0x0F)
- ]
+ eTagOpts ∷ [CmdOpt]
+ eTagOpts = filter (\ x → case x of
+ OptETag _ → True
+ _ → False) opts
- hex4bit ∷ Int → Char
- hex4bit n
- | n < 10 = chr $ ord '0' + n
- | n < 16 = chr $ ord 'a' + n - 10
- | otherwise = (⊥)
+ mkETagFromInput ∷ ETag
+ mkETagFromInput
+ = strToETag $ "SHA-1:" ⧺ showDigest (sha1 input)
+ strToETag ∷ String → ETag
+ strToETag str
+ = case A.fromChars str of
+ Just a → strongETag a
+ Nothing → error "ETag must not contain any non-ASCII letters."
openInput ∷ FilePath → IO Lazy.ByteString
openInput "-" = Lazy.getContents
openInput fpath = Lazy.readFile fpath
-
openOutput ∷ [CmdOpt] → IO Handle
openOutput opts
- = let outputOpts = filter (\ x → case x of
- OptOutput _ → True
- _ → False) opts
- in
- case outputOpts of
- [] → return stdout
- (OptOutput fpath):[] → openFile fpath WriteMode
- _ → error "two many --output options."
+ = case outputOpts of
+ [] → return stdout
+ (OptOutput fpath):[] → openFile fpath WriteMode
+ _ → fail "two many --output options."
+ where
+ outputOpts ∷ [CmdOpt]
+ outputOpts = filter (\ x → case x of
+ OptOutput _ → True
+ _ → False) opts
{-
作られるファイルの例 (壓縮されない場合):
Compressed Length: 453 bytes -- これは Compression: disabled の時には無い
Compression: disabled
MIME Type: image/png
- ETag: d41d8cd98f00b204e9800998ecf8427e
+ ETag: "d41d8cd98f00b204e9800998ecf8427e"
Last Modified: 2007-11-05 13:53:42.231882 JST
-}
{-# LANGUAGE OverloadedStrings #-}
= Just $ do foundEntity entityTag lastModified
setContentType contentType
- gzip ← isEncodingAcceptable "gzip"
- if gzip then
+ gzipAllowed ← isEncodingAcceptable "gzip"
+ if gzipAllowed then
do setContentEncoding ["gzip"]
output gzippedData
else