seonbi-0.2.0: app/seonbi-api.hs
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Control.Concurrent (threadDelay)
import Control.Monad
import Data.String
import Data.Version
import GHC.Exts (IsList (..))
import System.IO
import Data.Aeson
import qualified Data.Aeson.Types
import qualified Data.ByteString as B
import qualified Data.Map.Strict as M
import Data.Text
import Data.Text.Encoding
import Network.Wai
import Network.HTTP.Types
import Network.Wai.Handler.Warp
import Options.Applicative
import qualified Paths_seonbi as Meta
import Text.Seonbi.Facade
import Text.Seonbi.Trie as Trie
data Input = Input
{ sourceHtml :: Text
, configuration :: Configuration IO ()
} deriving (Show)
instance FromJSON Input where
parseJSON = withObject "Input" $ \ v -> do
sourceHtml' <- v .: "sourceHtml"
preset <- v .:? "preset"
config <- case preset of
Just locale ->
let presets' = presets :: M.Map Text (Configuration IO ())
m = M.lookup (replace "_" "-" $ toLower locale) presets'
in
case m of
Just p -> return p
Nothing -> fail $ unpack $ Data.Text.concat
[ "No such preset: "
, locale
, "; available presets: "
, intercalate ", " (M.keys presets')
]
Nothing -> do
xhtml' <- v .:? "xhtml" .!= False
quote' <- v .:? "quote"
cite' <- v .:? "cite"
arrow' <- v .:? "arrow"
ellipsis' <- v .:? "ellipsis" .!= False
emDash' <- v .:? "emDash" .!= False
stop' <- v .:? "stop"
hanja' <- v .:? "hanja" .!= Nothing
return Configuration
{ debugLogger = Nothing
, xhtml = xhtml'
, quote = quote'
, cite = cite'
, arrow = arrow'
, ellipsis = ellipsis'
, emDash = emDash'
, stop = stop'
, hanja = hanja'
}
return $ Input sourceHtml' config
instance FromJSON QuoteOption
instance FromJSON CiteOption
instance FromJSON ArrowOption
instance FromJSON StopOption
instance FromJSON HanjaRenderingOption
instance FromJSON HanjaOption where
parseJSON = withObject "HanjaOption" $ \ v -> HanjaOption
<$> v .: "rendering"
<*> v .: "reading"
instance FromJSON HanjaReadingOption where
parseJSON = withObject "HanjaReadingOption" $ \ v -> do
initialSoundLaw <- v .:? "initialSoundLaw" .!= False
wordMap <- v .:? "dictionary" .!= []
let wordPairs = GHC.Exts.toList (wordMap :: Object)
dictionary <- forM wordPairs $ \ (key, val) -> do
val' <- withText "Hangul string" return val
return (key, val')
let customDict = Trie.fromList dictionary
dictIds <- v .:? "useDictionaries" .!= []
useDictionaries <- forM (dictIds :: Array) $
withText "Dictionary ID string" getDictById
let dict = Prelude.foldl unionL customDict useDictionaries
return $ HanjaReadingOption initialSoundLaw dict
where
getDictById :: Text -> Data.Aeson.Types.Parser HanjaDictionary
getDictById "kr-stdict" = return southKoreanDictionaryUnsafe
getDictById dictId = fail ("No such dictionary ID: " ++ unpack dictId)
southKoreanDictionaryUnsafe :: HanjaDictionary
southKoreanDictionaryUnsafe = case hanja ko_KR' of
Just HanjaOption { reading = HanjaReadingOption { dictionary } } ->
dictionary
Nothing ->
Trie.empty
ko_KR' :: Configuration IO ()
ko_KR' = ko_KR
app :: AppOptions -> Application
app AppOptions { allowOrigin, debugDelayMs } request respond =
case requestMethod request of
"POST" -> do
inputJson <- lazyRequestBody request
threadDelay (debugDelayMs * 1000)
case eitherDecode' inputJson of
Right (Input source config) -> do
result <- transformHtmlText config source
respond' status200 $ object
[ "success" .= Bool True
, "resultHtml" .= String result
]
Left msg -> respond' status400 $ object
[ "success" .= Bool False
, "message" .= String (pack msg)
]
"OPTIONS" ->
respond' status200 Null
method -> respond' status405 $ object
[ "success" .= Bool False
, "message" .= String ("Unsupported method: " <> decodeUtf8 method)
]
where
respond' :: ToJSON a => Status -> a -> IO ResponseReceived
respond' status value' =
respond $ responseLBS status headers (encode value')
headers :: [Header]
headers = headerAdder
[ ("Content-Type", "application/json")
, ("Access-Control-Allow-Headers", "content-type")
]
headerAdder :: [Header] -> [Header]
headerAdder = case allowOrigin of
Just origin -> (("Access-Control-Allow-Origin", origin) :)
Nothing -> id
string :: IsString a => ReadM a
string = maybeReader (Just . fromString)
showHostPreference :: HostPreference -> String
showHostPreference h = case show h of
"HostAny" -> "[::]"
"HostIPv4" -> "0.0.0.0"
"HostIPv4Only" -> "0.0.0.0"
"HostIPv6" -> "[::]"
"HostIPv6Only" -> "[::]"
'H' : 'o' : 's' : 't' : ' ' : '"' : a ->
Prelude.take (Prelude.length a - 1) a
_ -> "?"
data CliOptions = CliOptions
{ serverSettings :: Settings
, appOptions :: AppOptions
}
data AppOptions = AppOptions
{ allowOrigin :: Maybe B.ByteString
, debugDelayMs :: Int
} deriving (Show, Eq)
parser :: Parser CliOptions
parser = CliOptions
<$> ( setHost
<$> option string
( long "host"
<> short 'H'
<> metavar "HOST"
<> value "*"
<> help "Host address to listen (default: [::/0])"
)
<*> ((`setPort` defaultSettings)
<$> option auto
( long "port"
<> short 'p'
<> metavar "PORT"
<> value 3800
<> showDefault
<> help "Port number to listen"
)
)
)
<*> ( AppOptions
<$> (
( Just <$> strOption
( long "allow-origin"
<> short 'o'
<> metavar "ORIGIN"
<> help "Allow cross-origin (i.e., CORS)"
)
)
<|> pure Nothing
)
<*> option auto
( long "debug-delay"
<> metavar "MS"
<> value 0
<> showDefault
<> help "Delay time for client development"
)
)
<**> helper
parserInfo :: ParserInfo CliOptions
parserInfo = info parser
( fullDesc
<> progDesc "Seonbi HTTP API server"
)
serverName :: B.ByteString
serverName =
"Seonbi/" `B.append` encodeUtf8 (pack $ showVersion Meta.version)
main :: IO ()
main = do
CliOptions
{ serverSettings = settings
, appOptions
} <- execParser parserInfo
let serverSettings' = setServerName serverName settings
let netloc = showHostPreference (getHost serverSettings') ++ ":" ++
show (getPort serverSettings')
let url = "http://" ++ netloc ++ "/"
hPutStrLn stderr url
runSettings serverSettings' $ app appOptions