Sunday, February 18, 2018

A configuration file (16)

We're going to need some configuration values for sending email, so I'll implement a very basic configuration file now.

To begin with, all it will contain is a single parameter to replace that hardcoded localhost:8080 in the source code. (trying to figure out something like that automatically is fraught with naivety and frustration)

I'll use the yaml yaml library to read in a config file formatted in yaml. For the simple key/value pair style of config file that I'm expecting this is about right. So add yaml to the library dependencies in package.yaml.

I'm going to base the configuration file around a Haskell data structure. Put this in a new file src/Config.hs:

module Config where

data Config = Config {
  urlbase :: String
}

Later on, any new configuration keys can be added into this.

I'd like to then have a function:

getConfig :: IO Config

... that returns the config.

The yaml module provides a function that reads YAML into an almost arbitrary data structure, returning either that structure or an error.

import qualified Data.Yaml as Y
...
getConfig :: IO Config
getConfig = do
  e <- Y.decodeFileEither "config.yaml"
  either
    (\err -> error $ "cannot read config file: " ++ show err)
    return
    e

decodeFileEither returns either an error (on the Left), or the loaded Config (on the Right). It knows that the output is a Config by inference from the type signature of getConfig - so in this case, the type signature isn't just pretty decoration.

If the config file can't be read, we'll error out and let someone else handle the mess. If this happens inside a servant request handler for example, it'll return a 500 error code to the remote client.

This won't compile though. decodeFileEither can't decode into any arbitrary data structure. It actually needs the destination class to be an instance of ToJSON (because YAML is a relative of JSON, and the yaml module uses that fact). Luckily it's possible to use generics for this again:

import qualified GHC.Generics as G
...
data Config = Config {
  urlbase :: String
} deriving (G.Generic, Y.FromJSON)

Now we can use this. In app/Main.hs, find doInvitation and add:

config <- getConfig

... somewhere in there. And now the code for generating the URL can change from:

let url = "http://localhost:8080/registration/" ++ newNonce

... to ...

let url = (urlbase config) ++ "/registration/" ++ newNonce

Here's an example configuration file that uses localhost still:

urlbase: "http://localhost:8080"

You can change that localhost to, for example, your PC's LAN IP address and then perhaps be able to register for events from your phone on the same LAN.

Used in this way, the config file is re-read every time doInvitation runs. For the light load I'm expecting I'm not massively fussed about this. A more Haskelly approach would be to read the configuration once at startup and thread it through the program where needed using ReaderT. That would also mean the whole program would see a consistent configuration even if the configuration file is changed; which may be a good thing or a bad thing.

Here's today's commit: f1bbe3a2. There's a config.yaml.example in there, that you'll have to copy into place. The git repo is also configured to ignore the live version of the configuration file so that it won't be committed to version control, in .gitignore. This is because that configuration is going to contain secrets in the future, and version control is a hilarious way to leak your secrets.

Next I'm going to get some basic email sending working.

No comments:

Post a Comment