Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do I elegantly handle routes with multiple parameters? #34

Closed
tmbull opened this issue Aug 15, 2017 · 5 comments
Closed

How do I elegantly handle routes with multiple parameters? #34

tmbull opened this issue Aug 15, 2017 · 5 comments

Comments

@tmbull
Copy link

tmbull commented Aug 15, 2017

Hi, first of all, I apologize if this is a general Haskell question but I'm currently trying to use servant-auth-cookie to add authentication to an API with several endpoints. The issue is that most of my endpoints have several query parameters and/or captures. I struggled with this for a while, and eventually ended up rolling my own version for handlers of arbitrary arity:

type SimpleApi = "simple"
                   :> AuthProtect "cookie-auth"
                   :> Get '[JSON] (Cookied String)
                 :<|> "one-param"
                   :> QueryParam "q" String
                   :> AuthProtect "cookie-auth"
                   :> Get '[JSON] (Cookied String)
                 :<|> "two-param"
                   :> QueryParam "q1" String
                   :> QueryParam "q2" String
                   :> AuthProtect "cookie-auth"
                   :> Get '[JSON] (Cookied String)

simple ::
       Session
    -> String
simple session = "foobar"

simple1 ::
     Maybe String
  -> Session
  -> String
simple1 q session = show q

simple2 ::
     Maybe String
  -> Maybe String
  -> Session
  -> String
simple2 q1 q2 session = show q1 ++ show q2

server :: (ServerKeySet s)
  => AuthCookieSettings
  -> RandomSource
  -> s
  -> Server SimpleApi
server settings rs sks = simple' :<|> simple1' :<|> simple2'
  where
    simple' = cookied settings rs sks simple
    simple1' = cookied1 settings rs sks simple1
    simple2' = cookied2 settings rs sks simple2

It seems like ought to be an easier solution. Am I doing this wrong? Thanks!

@zohl
Copy link
Owner

zohl commented Aug 17, 2017

Hello,

no need to apologize, there is indeed design flaw in cookied function. I didn't think of using it in tandem with query parameters and therefore it's hard to use it this way.

As a temporary workaround you can use cookies with lambdas like that:

handler  q1 q2 ... qN session = ...
handler' q1 q2 ... qN         = cookied (handler q1 q2)  -- here session variable can be omitted

-- or when session argument goes first
handler  session q1 q2 ... qN = ...
handler' session q1 q2 ... qN = cookied (\s -> handler s q1 q2 ... qN) session

Looks ugly, but should work. I'm thinking how to simplify it.

The problem is that cookies should transform function of type
t1 -> t2 -> ... -> tN -> Session -> r to
t1 -> t2 -> ... -> tN -> WithMetadata Session -> Handler (Cookied r), i.e. wrap the last argument and lift result to Handler monad.
Dealing with "the last argument" when we don't know exact number of arguments is quite tricky.
Even if we rewrite the code, so that the session is the first argument, we will have to drag Handler monad through applications of each argument, which results in more complicated and verbose code.

As a side note, there is one more problem: cookied takes pure function (in a sesnse that it's result is not in Handler monad). This means you cannot throw errors inside it. Unlike the first problem, it's easier to fix.

I have few ideas, I would like try before resorting to template haskell, so it might take the time :)

@zohl
Copy link
Owner

zohl commented Aug 18, 2017

Done, it is possible without TH. Here is an example of new cookied function:

type TestApi
  =    "test-0-0"
    :> AuthProtect "cookie-auth"
    :> Get '[JSON] (Cookied String)
  :<|> "test-1-0"
    :> AuthProtect "cookie-auth"
    :> QueryParam "q1" String
    :> Get '[JSON] (Cookied String)
  :<|> "test-1-1"
    :> QueryParam "q1" String
    :> AuthProtect "cookie-auth"
    :> Get '[JSON] (Cookied String)
  :<|> "test-2-0"
    :> AuthProtect "cookie-auth"
    :> QueryParam "q1" String
    :> QueryParam "q2" Int
    :> Get '[JSON] (Cookied String)
  :<|> "test-2-1"
    :> QueryParam "q1" String
    :> AuthProtect "cookie-auth"
    :> QueryParam "q2" Int
    :> Get '[JSON] (Cookied String)
  :<|> "test-2-2"
    :> QueryParam "q1" String
    :> QueryParam "q2" Int
    :> AuthProtect "cookie-auth"
    :> Get '[JSON] (Cookied String)
  :<|> "test-3-0"
    :> AuthProtect "cookie-auth"
    :> QueryParam "q1" String
    :> QueryParam "q2" Int
    :> QueryParam "q3" Bool
    :> Get '[JSON] (Cookied String)
  :<|> "test-3-1"
    :> QueryParam "q1" String
    :> AuthProtect "cookie-auth"
    :> QueryParam "q2" Int
    :> QueryParam "q3" Bool
    :> Get '[JSON] (Cookied String)
  :<|> "test-3-2"
    :> QueryParam "q1" String
    :> QueryParam "q2" Int
    :> AuthProtect "cookie-auth"
    :> QueryParam "q3" Bool
    :> Get '[JSON] (Cookied String)
  :<|> "test-3-3"
    :> QueryParam "q1" String
    :> QueryParam "q2" Int
    :> QueryParam "q3" Bool
    :> AuthProtect "cookie-auth"
    :> Get '[JSON] (Cookied String)


test0 :: Session -> Handler String
test0 session = return . concat $ (["test-0-0::", show session] :: [String])

test10 :: Session -> Maybe String -> Handler String
test10 session q1 = return . concat $ (["test-1-0::", show q1, "::", show session] :: [String])

test11 :: Maybe String -> Session -> Handler String
test11 q1 session = return . concat $ (["test-1-1::", show q1, "::", show session] :: [String])

test20 :: Session -> Maybe String -> Maybe Int -> Handler String
test20 session q1 q2 = return . concat $ (["test-2-0::", show q1, "::", show q2, "::", show session] :: [String])

test21 :: Maybe String -> Session -> Maybe Int -> Handler String
test21 q1 session q2 = return . concat $ (["test-2-1::", show q1, "::", show q2, "::", show session] :: [String])

test22 :: Maybe String -> Maybe Int -> Session -> Handler String
test22 q1 q2 session = return . concat $ (["test-2-2::", show q1, "::", show q2, "::", show session] :: [String])

test30 :: Session -> Maybe String -> Maybe Int -> Maybe Bool -> Handler String
test30 session q1 q2 q3 = return . concat $ (["test-3-0::", show q1, "::", show q2, "::", show q3, "::", show session] :: [String])

test31 :: Maybe String -> Session -> Maybe Int -> Maybe Bool -> Handler String
test31 q1 session q2 q3 = return . concat $ (["test-3-1::", show q1, "::", show q2, "::", show q3, "::", show session] :: [String])

test32 :: Maybe String -> Maybe Int -> Session -> Maybe Bool -> Handler String
test32 q1 q2 session q3 = return . concat $ (["test-3-2::", show q1, "::", show q2, "::", show q3, "::", show session] :: [String])

test33 :: Maybe String -> Maybe Int -> Maybe Bool -> Session -> Handler String
test33 q1 q2 q3 session = return . concat $ (["test-3-3::", show q1, "::", show q2, "::", show q3, "::", show session] :: [String])


serveTest
  =    cookied' test0
  :<|> cookied' test10
  :<|> cookied' test11
  :<|> cookied' test20
  :<|> cookied' test21
  :<|> cookied' test22
  :<|> cookied' test30
  :<|> cookied' test31
  :<|> cookied' test32
  :<|> cookied' test33
  where
    cookied' :: CookiedWrapper Session
    cookied' = cookied settings rs sks (Proxy :: Proxy Session)

So, to use it you should do the following:

  1. check that your handlers return value in Handler monad (in the example it's simply return).
  2. enable FlexibleContexts extension

The price of such wrapper is obscure compiler error messages when something goes wrong. In case of ambiguity, make sure you provided signature for every function you pass to cookied.

@tmbull
Copy link
Author

tmbull commented Aug 18, 2017

Awesome! Thanks for the quick update. I'll give it a try and let you know how it goes.

@tmbull
Copy link
Author

tmbull commented Aug 19, 2017

@zohl wanted to let you know that I tried the new cookied in our project and it works great, so I'll go ahead and close this. Thanks again for the quick response.

@tmbull tmbull closed this as completed Aug 19, 2017
@zohl
Copy link
Owner

zohl commented Aug 19, 2017

Alright, thank you for letting me know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants