This commit is contained in:
Mats Rauhala 2019-10-15 23:47:32 +03:00
parent 9e3fb00b44
commit 46a8394b4e
10 changed files with 184 additions and 0 deletions

View File

@ -4,6 +4,19 @@
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-|
Module : API
Description : The application API and handlers
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
Provides the API and handlers for the application.
Even if this is only one module, I'm using this same style in all of my projects. Each level of APIs provide their own `API` type and a `handler` who is responsible only for that specific subsection of an API.
-}
module API where module API where
import Control.Monad.Reader (MonadReader, asks) import Control.Monad.Reader (MonadReader, asks)
@ -25,11 +38,15 @@ import Data.Language
import Solidabis.API (HasClientEnv, HasToken, bullshits, import Solidabis.API (HasClientEnv, HasToken, bullshits,
getBullshits, message) getBullshits, message)
-- | Data for the frontpage
--
-- Our "UI" only cares about the decrypted and non-decryptable sentences
data Index = data Index =
Index { converted :: [Text] Index { converted :: [Text]
, unconverted :: [Text] , unconverted :: [Text]
} }
-- | Wrap a UI within an HTML page
page :: Monad m => HtmlT m () -> HtmlT m () page :: Monad m => HtmlT m () -> HtmlT m ()
page = doctypehtml_ . body_ page = doctypehtml_ . body_
@ -43,10 +60,16 @@ instance ToHtml Index where
ul_ $ traverse_ (li_ . toHtml) unconverted ul_ $ traverse_ (li_ . toHtml) unconverted
toHtmlRaw = toHtml toHtmlRaw = toHtml
-- | Our API type
--
-- Consists only of the index route
newtype API route newtype API route
= API { _index :: route :- Get '[HTML] Index } = API { _index :: route :- Get '[HTML] Index }
deriving Generic deriving Generic
-- | API handler
--
-- Compares the sentences given by the solidabis api against the model
handler handler
:: (HasModel r, MonadReader r m, HasToken r, HasClientEnv r, MonadIO m) :: (HasModel r, MonadReader r m, HasToken r, HasClientEnv r, MonadIO m)
=> API (AsServerT m) => API (AsServerT m)

View File

@ -1,23 +1,43 @@
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-|
Module : Control.Lens
Description : Simplified functional lenses
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
A very small subset of the lens library reimplemented
See http://hackage.haskell.org/package/lens for proper docs
-}
module Control.Lens where module Control.Lens where
import Control.Monad.Reader (MonadReader, asks) import Control.Monad.Reader (MonadReader, asks)
import Data.Functor.Const (Const (..)) import Data.Functor.Const (Const (..))
import Data.Functor.Identity (Identity (..)) import Data.Functor.Identity (Identity (..))
-- | Full lens type
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
-- | Simplified lens type
type Lens' s b = Lens s s b b type Lens' s b = Lens s s b b
-- | Build a lens out of a getter and a setter
lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b
lens sa sbt afb s = sbt s <$> afb (sa s) lens sa sbt afb s = sbt s <$> afb (sa s)
-- | Getting a value
type Getting r s a = (a -> Const r a) -> s -> Const r s type Getting r s a = (a -> Const r a) -> s -> Const r s
-- | View a value
view :: MonadReader r m => Getting a r a -> m a view :: MonadReader r m => Getting a r a -> m a
view l = asks (getConst . l Const) view l = asks (getConst . l Const)
-- | Settering a value
type ASetter s t a b = (a -> Identity b) -> s -> Identity t type ASetter s t a b = (a -> Identity b) -> s -> Identity t
-- | Set a value
set :: ASetter s t a b -> b -> s -> t set :: ASetter s t a b -> b -> s -> t
set l b = runIdentity . l (\_ -> Identity b) set l b = runIdentity . l (\_ -> Identity b)

View File

@ -1,5 +1,20 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RankNTypes #-}
{-|
Module : Control.Monad.App
Description : Our application monad
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
Provides the application monad, which is just a newtype over a ReaderT environment.
The environment contains the configuration as well as values determind at startup.
The environment can be thought of as dependency injection.
-}
module Control.Monad.App where module Control.Monad.App where
import Control.Monad.Reader (MonadReader, ReaderT, runReaderT) import Control.Monad.Reader (MonadReader, ReaderT, runReaderT)
@ -14,26 +29,33 @@ import Solidabis.API (HasClientEnv (..), HasToken (..), Token)
-- | Lens for accessing token
accessToken :: Lens' Config Token accessToken :: Lens' Config Token
accessToken = lens _accessToken (\st x -> st{_accessToken=x}) accessToken = lens _accessToken (\st x -> st{_accessToken=x})
-- | Lens for accessing solidabis base url
solidabisBase :: Lens' Config BaseUrl solidabisBase :: Lens' Config BaseUrl
solidabisBase = lens _solidabisBase (\st x -> st{_solidabisBase=x}) solidabisBase = lens _solidabisBase (\st x -> st{_solidabisBase=x})
-- | Lens for accessing the server port
port :: Lens' Config Port port :: Lens' Config Port
port = lens _port (\st x -> st{_port = x}) port = lens _port (\st x -> st{_port = x})
-- | The environment
data App data App
= App { _config :: Config = App { _config :: Config
, _solidabisClient :: ClientEnv , _solidabisClient :: ClientEnv
, _languageModel :: Model} , _languageModel :: Model}
-- | Lens for accessing the config
config :: Lens' App Config config :: Lens' App Config
config = lens _config (\st x -> st{_config=x}) config = lens _config (\st x -> st{_config=x})
-- | Lens for accessing the client environment
solidabisClient :: Lens' App ClientEnv solidabisClient :: Lens' App ClientEnv
solidabisClient = lens _solidabisClient (\st x -> st{_solidabisClient=x}) solidabisClient = lens _solidabisClient (\st x -> st{_solidabisClient=x})
-- | Lens for accessing the language model
model :: Lens' App Model model :: Lens' App Model
model = lens _languageModel (\st x -> st{_languageModel=x}) model = lens _languageModel (\st x -> st{_languageModel=x})
@ -53,8 +75,12 @@ instance HasModel App where
getModel = view model getModel = view model
setModel app tk = set model tk app setModel app tk = set model tk app
-- | The application monad
--
-- This is just a 'ReaderT App IO'
newtype AppM a = AppM (ReaderT App IO a) newtype AppM a = AppM (ReaderT App IO a)
deriving (Functor, Applicative, Monad, MonadReader App, MonadIO) deriving (Functor, Applicative, Monad, MonadReader App, MonadIO)
-- | Run the application monad into IO
runAppM :: App -> AppM a -> IO a runAppM :: App -> AppM a -> IO a
runAppM st (AppM f) = runReaderT f st runAppM st (AppM f) = runReaderT f st

View File

@ -1,7 +1,21 @@
{-|
Module : Data.Caesar
Description : Utilities for doing caesar transforms
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
-}
module Data.Caesar where module Data.Caesar where
-- | Type alias over char
type Caesar = Char type Caesar = Char
-- | Translate to the next character on the left
--
-- Has special handling for some symbols
next :: Char -> Char next :: Char -> Char
next c = next c =
case c of case c of
@ -13,5 +27,6 @@ next c =
'a' -> 'ö' 'a' -> 'ö'
x -> pred x x -> pred x
-- | Translate a character n times
caesar :: Int -> Caesar -> Caesar caesar :: Int -> Caesar -> Caesar
caesar n x = foldr ($) x (replicate n next) caesar n x = foldr ($) x (replicate n next)

View File

@ -1,4 +1,15 @@
{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DeriveGeneric #-}
{-|
Module : Config
Description : Config types
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
The config type with a helper function for reading the config from disk
-}
module Data.Config where module Data.Config where
import Server (Port) import Server (Port)
@ -10,6 +21,9 @@ import Data.Yaml.Config
import GHC.Generics (Generic) import GHC.Generics (Generic)
import Servant.Client (BaseUrl) import Servant.Client (BaseUrl)
-- | Config type
--
-- These are all values that should be known statically beforehand
data Config data Config
= Config { _accessToken :: Token = Config { _accessToken :: Token
, _solidabisBase :: BaseUrl , _solidabisBase :: BaseUrl
@ -19,5 +33,6 @@ data Config
instance FromJSON Config instance FromJSON Config
-- | Read the configuration yaml from file
readConfigFromFile :: MonadIO m => FilePath -> m Config readConfigFromFile :: MonadIO m => FilePath -> m Config
readConfigFromFile path = liftIO $ loadYamlSettings [path] [] ignoreEnv readConfigFromFile path = liftIO $ loadYamlSettings [path] [] ignoreEnv

View File

@ -1,4 +1,17 @@
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-|
Module : Data.Language
Description : The language model
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
An n-gram model over a language. Should be trained with regular text.
The idea behind this module is to compare a sentence n-grams against the model. Higher the results, higher the probability of it being the same language as the model.
-}
module Data.Language where module Data.Language where
import Data.Foldable (fold) import Data.Foldable (fold)
@ -10,10 +23,12 @@ import Data.Text (Text)
import Data.NGram import Data.NGram
-- | Model represents the language
data Model data Model
= Model { parts :: Map Text (Sum Int) = Model { parts :: Map Text (Sum Int)
, total :: Sum Int } , total :: Sum Int }
-- | Has* style pattern for accessing the language model
class HasModel a where class HasModel a where
getModel :: a -> Model getModel :: a -> Model
setModel :: a -> Model -> a setModel :: a -> Model -> a
@ -22,15 +37,20 @@ instance HasModel Model where
getModel = id getModel = id
setModel = const setModel = const
-- | The default n-gram size
ngramSize :: Int ngramSize :: Int
ngramSize = 3 ngramSize = 3
-- | Build a language model out of a corpus content
buildModel :: Text -> Model buildModel :: Text -> Model
buildModel str = buildModel str =
let parts = ngram ngramSize str let parts = ngram ngramSize str
total = fold parts total = fold parts
in Model{..} in Model{..}
-- | Calculate the 'goodness' of a sentence
--
-- Higher numbers means higher probability of being part of the same language as the model
goodness :: Model -> Text -> Double goodness :: Model -> Text -> Double
goodness Model{..} str = goodness Model{..} str =
let comparison = M.keys $ ngram ngramSize str let comparison = M.keys $ ngram ngramSize str

View File

@ -1,4 +1,15 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-|
Module : Data.NGram
Description : N-gram utilities
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
Utilities for building n-gram models
-}
module Data.NGram where module Data.NGram where
import Data.List (unfoldr) import Data.List (unfoldr)
@ -8,6 +19,7 @@ import Data.Monoid (Sum (..))
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
-- | Build a n-gram frequency map
ngram :: Int -> Text -> Map Text (Sum Int) ngram :: Int -> Text -> Map Text (Sum Int)
ngram n = M.unionsWith (<>) . unfoldr go ngram n = M.unionsWith (<>) . unfoldr go
where where

View File

@ -3,6 +3,18 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-|
Module : Main
Description : The main module
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
The main access point for the koodihaaste
-}
module Main where module Main where
import Control.Monad.App import Control.Monad.App
@ -14,11 +26,16 @@ import Options.Generic
import Servant.Client import Servant.Client
import Server import Server
-- | The command line endpoint
--
-- Parses only the path to the config file
-- see config.yaml.sample for the format
newtype Cmd = Cmd (FilePath <?> "Config path") newtype Cmd = Cmd (FilePath <?> "Config path")
deriving Generic deriving Generic
instance ParseRecord Cmd instance ParseRecord Cmd
-- | The main function
main :: IO () main :: IO ()
main = do main = do
Cmd (Helpful configPath) <- getRecord "koodihaaste" Cmd (Helpful configPath) <- getRecord "koodihaaste"

View File

@ -1,4 +1,15 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-|
Module : Server
Description : Run the http server
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
-}
module Server where module Server where
import Control.Monad.Reader (MonadReader, ask, asks, runReaderT) import Control.Monad.Reader (MonadReader, ask, asks, runReaderT)
@ -12,8 +23,14 @@ import API (handler)
import Data.Language (HasModel) import Data.Language (HasModel)
import Solidabis.API (HasClientEnv, HasToken) import Solidabis.API (HasClientEnv, HasToken)
-- | The port to run on
--
-- Should be provided within the environment
newtype Port = Port Int deriving (Show, FromJSON) newtype Port = Port Int deriving (Show, FromJSON)
-- | Has* pattern
--
-- Locate the port within the larger environment structure
class HasPort a where class HasPort a where
getPort :: a -> Port getPort :: a -> Port
setPort :: a -> Port -> a setPort :: a -> Port -> a
@ -22,9 +39,11 @@ instance HasPort Port where
getPort = id getPort = id
setPort = const setPort = const
-- | Convert the servant handler into wai application
app :: (HasModel r, HasClientEnv r, HasToken r) => r -> Application app :: (HasModel r, HasClientEnv r, HasToken r) => r -> Application
app env = genericServeT (`runReaderT` env) API.handler app env = genericServeT (`runReaderT` env) API.handler
-- | Run the server
server server
:: (HasModel r, HasClientEnv r, HasToken r, HasPort r, MonadReader r m, MonadIO m) :: (HasModel r, HasClientEnv r, HasToken r, HasPort r, MonadReader r m, MonadIO m)
=> m () => m ()

View File

@ -4,6 +4,17 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-|
Module : Solidabis.API
Description : The client API for solidabis API
Copyright : (c) Mats Rauhala, 2019
License : BSD3
Maintainer : mats.rauhala@iki.fi
Stability : experimental
Portability : POSIX
Provides the client API for the solidabis API. Implemented through servant generics.
-}
module Solidabis.API module Solidabis.API
( getBullshits ( getBullshits
, Bullshit(..) , Bullshit(..)
@ -28,9 +39,11 @@ import Servant.Client.Core.Request (Request)
import qualified Servant.Client.Core.Request as Req import qualified Servant.Client.Core.Request as Req
import Servant.Client.Generic import Servant.Client.Generic
-- | JSON representation of the message layer
newtype Message = Message { message :: Text } newtype Message = Message { message :: Text }
deriving Generic deriving Generic
-- | JSON representation of the bullshit layer
newtype Bullshit newtype Bullshit
= Bullshit { bullshits :: [ Message ] } = Bullshit { bullshits :: [ Message ] }
deriving Generic deriving Generic
@ -42,6 +55,7 @@ newtype API route
= API { _bullshit :: route :- AuthProtect "jwt" :> "bullshit" :> Get '[JSON] Bullshit } = API { _bullshit :: route :- AuthProtect "jwt" :> "bullshit" :> Get '[JSON] Bullshit }
deriving (Generic) deriving (Generic)
-- | Has* style pattern for accessing the client environment
class HasClientEnv a where class HasClientEnv a where
getClientEnv :: a -> ClientEnv getClientEnv :: a -> ClientEnv
setClientEnv :: a -> ClientEnv -> a setClientEnv :: a -> ClientEnv -> a
@ -54,9 +68,11 @@ routes :: (MonadReader r m, MonadIO m, HasClientEnv r) => API (AsClientT m)
routes = genericClientHoist routes = genericClientHoist
(\x -> asks getClientEnv >>= \env -> liftIO (runClientM x env >>= either throwIO return)) (\x -> asks getClientEnv >>= \env -> liftIO (runClientM x env >>= either throwIO return))
-- | Newtype wrapper over the jwt token
newtype Token = Token String newtype Token = Token String
deriving (Semigroup, Monoid, ToHttpApiData, IsString, Show, FromJSON) deriving (Semigroup, Monoid, ToHttpApiData, IsString, Show, FromJSON)
-- | Has* style pattern for accessing the authentication token
class HasToken a where class HasToken a where
getToken :: a -> Token getToken :: a -> Token
setToken :: a -> Token -> a setToken :: a -> Token -> a
@ -70,6 +86,7 @@ type instance AuthClientData (AuthProtect "jwt") = Token
authReq :: Token -> Request -> Request authReq :: Token -> Request -> Request
authReq token = Req.addHeader "Authorization" ("Bearer " <> token) authReq token = Req.addHeader "Authorization" ("Bearer " <> token)
-- | Return the encoded sentences from the solidabis API
getBullshits getBullshits
:: (HasToken r, MonadReader r m, MonadIO m, HasClientEnv r) :: (HasToken r, MonadReader r m, MonadIO m, HasClientEnv r)
=> m Bullshit => m Bullshit