4

I'm using custom monad (with reader) to easily pass data like DB pool to my handlers (before using custom monad I used to pass connection as fn argument).

This is how I've defined my custom monad :

newtype Controller a = Controller
    { runController :: ReaderT ServerEnvironment Handler a
    } deriving ( Functor, Applicative, Monad, MonadReader ServerEnvironment, 
                 MonadError ServantErr, MonadIO )

This ServerEnvironment is just custom datatype I use to carry my data.

Problem is that for my AuthHandler I have to specifically use function with:

r -> Handler usr

as authentication handler, I can't use my custom handler which would be :

r -> Controller usr

and I also have no way to pass in my ConnectionPool because signature can't be :

ConnPool -> r -> Handler usr

So, how does one pass extra data to authentication handler in servant without using global IO state?

Reygoch
  • 1,204
  • 1
  • 11
  • 24

1 Answers1

7

The AuthHandler you put into the context doesn't have to be defined at the top-level! Generally, you'll want to do it in main so you have access to the database connections etc. which you've created:

type API = 
  ... :<|> (AuthProtect "myProtection" :> ...) :<|> ...

type instance AuthServerData (AuthProtect "myProtection") = User

server :: ServerEnvironment -> Server API
server env = ...

setupEnv :: IO ServerEnvironment
setupEnv = ..

-- This is essentially a 'Controller'.
authenticate :: ServerEnvironment -> Handler User
authenticate conn = ...

main :: IO ()
main = do
  env <- setupEnv
  -- Now, because we have access to the env, we can turn our
  -- 'authenticate' into the right type before putting it
  -- in the context
  let ctx = authenticate env :. EmptyContext
  run 8080 $ serveWithContext myAPI (server conn) ctx
user2141650
  • 2,827
  • 1
  • 15
  • 23
  • still, is it not possible to use custom monad for authentication handler? – Reygoch Dec 25 '16 at 18:33
  • It is - we could have given `authenticate` the signature `Controller User`, and then had `let ctx = runReader (runController authenticate) env :. EmptyContext`. – user2141650 Dec 25 '16 at 18:41
  • mmm.. yes, but than I have to pass env to both my auth context and my regular controllers. I'm using `enter (convertController env) serverRoutes` to convert my `Controllers` to `Handlers` if I wanted to use `State` in my stack than that state would be out of sync for Authentication handler and my regular handlers. – Reygoch Dec 25 '16 at 18:48
  • True, though you can use the `Request`s `Vault` (or an `IORef`) if you want to thread state from the authentication handler to the normal handler. – user2141650 Dec 25 '16 at 20:33
  • 1
    You can't use `State` in your handlers for sharing and modifying state from different handlers or from within the same handler but on 2 different requests: using `State` for handlers only lets you read/modify some state within one run of a given handler on a given request. Regarding the question itself: there's nothing (yet) that lets one easily call `enter` once and have it be applied to both the auth context and the handlers. Those are two very separate things, the context might contain various kinds of stuffs, some of which we don't want to apply this transformation to. – Alp Mestanogullari Dec 25 '16 at 20:34