Re: Snap Predicates

7 views
Skip to first unread message

Gregory Collins

unread,
Nov 30, 2011, 2:30:32 PM11/30/11
to Toralf Wittner, sn...@snapframework.com
On Mon, Nov 28, 2011 at 5:05 PM, Toralf Wittner
<toralf....@gmail.com> wrote:
> Hi Gregory,
> Apologies in advance for contacting you directly but I would like to
> discuss some aspect related to Snap and would like to hear your opinion
> about it.
> I recently started using Snap to reimplement some webservice based on an
> existing one which was implemented in Java/Spring. So far I really enjoy
> using Snap. Compared to Spring MVC it is a breath of fresh air ;)
> In particular I like the possibility to declare routes at some central
> place. What I wanted to do is declare the paths and then constrain the
> handler functions by some request predicates. I have seen 'method' et
> al. but they just pass the bucket if their condition is not true. This
> results for instance in 404 responses when a handler is constrained to a
> GET method, instead of a 405 which would be more appropriate. Now I know
> that one can express all those restrictions directly in handler functions
> but maybe a more declarative way is desirable.
> Would it be useful to have a module Snap.Request.Predicates which contains a
> couple of these request predicates which can be used directly in route
> declarations like this (suppose 'echo' just creates a response with the
> given body string):
>
> [ ("x",       when (method GET)                                      (echo
> "x"))
> , ("x/y",     when (method GET <&> param "q" (== "123"))             (echo
> "x/y"))
> , ("x/y/z",   when (hasHeader "foo")                                 (echo
> "x/y/z"))
> , ("x/y/z/a", when (method GET <&> (hasCookie "q" <|> hasParam "q")) (echo
> "x/y/z/a")) ]
> Alternatively one could also put these in handler functions, e.g.
> foo :: Snap ()
> foo = when (method GET <&> valParam "x") $
>     getParam "x" >>= echo . fromJust
> (N.B. The names/symbols 'when', 'method', '<|>', etc. obviously clash with
> existing ones from other modules. I have just used them here like this
> for illustrative purposes.)
> Of course predicates become less useful the more complicated they are.
> However what I like about them is the error reporting as well as the
> guarantees that simple '<&>'-chains give me.
> I came up with some initial implementation here:
> https://gist.github.com/6b8181c6aaad3b4ae81e
> Do you think something like this would be good to have as part of Snap,
> or rather not? One can of course easily put these predicates in some
> other package, so what do you think? Or did I maybe overlook something
> in Snap and similar functionality already exists?
> Sorry again for bothering you, I hope it is okay.
> Cheers,
> Toralf
>

Hi Toralf,

I'll give this its proper attention soon but for now I'm just going to
forward your message to the snap list.

G
--
Gregory Collins <gr...@gregorycollins.net>

Gregory Collins

unread,
Dec 13, 2011, 2:55:47 PM12/13/11
to Toralf Wittner, sn...@snapframework.com
Hi Toralf,

I hope you'll forgive me for the egregious delay in getting back to you on this.

Just based on having a quick look at your email, I think that you've identified a problem where our API could do a better job of expressing an idiom. About your proposed combinator set; is there a reason you preferred "... -> m Bool" to the MonadPlus/Alternative semantics? I think a lot of the things you were trying to do with the gist you posted can be expressed in those terms. For instance:

    when :: MonadSnap m => m Bool -> m a -> m a
    when p m = p >>= result
      where
        result True  = m
        result False = getResponse >>= finishWith

Can be written as

    when :: MonadSnap m => m Bool -> m a -> m a
    when p m = (p >>= guard >> m) <|> (getResponse >>= finishWith)

Your example about "method" can also be expressed in these terms:

    methodOr405 :: MonadSnap m => Method -> m a -> m a
    methodOr405 m act = method m act <|> serve405
      where
        -- do other stuff here
        serve405 = finishWith $ setResponseCode 405 $ emptyResponse

I'd encourage you to see how many of the things in the gist you posted can be written this way. We don't always specify concrete behaviour for things like returning 405 for method calls, because we don't always want to be too tied into things which could be considered policy decisions. Stipulating that method calls a finishWith also precludes constructs like "method GET a <|> method POST b".

That said, there are probably convenient guards we're missing, like:

    guardParam :: MonadSnap m => ByteString -> (ByteString -> Bool) -> m ()
    guardParam k p = getParam k >>= maybe pass (guard . p)

    withParam :: MonadSnap m => ByteString -> (ByteString -> m a) -> m a
    withParam k m = getParam k >>= maybe pass m

    guardHeader :: MonadSnap m => CI ByteString -> (ByteString -> Bool) -> m ()
    guardHeader h p = getsRequest (getHeader h) >>= maybe pass (guard . p)

    withHeader :: MonadSnap m => CI ByteString -> (ByteString -> m a) -> m a
    withHeader h m = getsRequest (getHeader h) >>= maybe pass m

    withCookie :: MonadSnap m => ByteString -> (Cookie -> m a) -> m a
    withCookie c m = getCookie c >>= maybe pass m

What do you think?

G


--
Gregory Collins <gr...@gregorycollins.net>

Toralf Wittner

unread,
Dec 16, 2011, 2:37:30 AM12/16/11
to Gregory Collins, sn...@snapframework.com
On 13 December 2011 20:55, Gregory Collins <gr...@gregorycollins.net> wrote:
Hi Toralf,

I hope you'll forgive me for the egregious delay in getting back to you on this.

No problem at all. Sorry for being somewhat late myself.
 

Just based on having a quick look at your email, I think that you've identified a problem where our API could do a better job of expressing an idiom. About your proposed combinator set; is there a reason you preferred "... -> m Bool" to the MonadPlus/Alternative semantics? I think a lot of the things you were trying to do with the gist you posted can be expressed in those terms. For instance:

m Bool felt natural for writing logical combinators, as for instance or needs to know if at least one of the predicates was satisfied in order to potentially reset any responses set up by a predicate, e.g. the first predicate is false, but the second is true. MonadPlus/Alternative did not appear to be as easy to use for this, but maybe I missed something obvious?
 

    when :: MonadSnap m => m Bool -> m a -> m a
    when p m = p >>= result
      where
        result True  = m
        result False = getResponse >>= finishWith

Can be written as

    when :: MonadSnap m => m Bool -> m a -> m a
    when p m = (p >>= guard >> m) <|> (getResponse >>= finishWith)

Yes, when is really just evaluating the predicate and invoking the handler function if the predicate is satisfied. More interesting would be the logical or connective for predicates.
 

Your example about "method" can also be expressed in these terms:

    methodOr405 :: MonadSnap m => Method -> m a -> m a
    methodOr405 m act = method m act <|> serve405
      where
        -- do other stuff here
        serve405 = finishWith $ setResponseCode 405 $ emptyResponse

 
I'd encourage you to see how many of the things in the gist you posted can be written this way. We don't always specify concrete behaviour for things like returning 405 for method calls, because we don't always want to be too tied into things which could be considered policy decisions. Stipulating that method calls a finishWith also precludes constructs like "method GET a <|> method POST b".

This is probably the core issue. I wanted to express something like, "if the request has a cookie 'c' and a parameter 'p', only then invoke action a", and during evaluation of this condition the appropriate error response would already be set up. With many pre-conditions which could not be fulfilled it can become tiresome to write appropriate error responses manually. For example:

when (method GET <&> hasCookie "q" <&> hasParam "x") handler

would return 405 if the method is not GET, 400 ("invalid cookie: q") if the cookie q is not set and, 400 ("invalid parameter: x") if x is not there. Of course I never intended to replace the existing Snap combinators like method because I know that sometimes method GET a <|> method POST b is exactly what is needed. But I see your point about avoiding concrete behaviour in the general Snap API and this approach may just not be flexible enough.
 

That said, there are probably convenient guards we're missing, like:

    guardParam :: MonadSnap m => ByteString -> (ByteString -> Bool) -> m ()
    guardParam k p = getParam k >>= maybe pass (guard . p)

    withParam :: MonadSnap m => ByteString -> (ByteString -> m a) -> m a
    withParam k m = getParam k >>= maybe pass m

    guardHeader :: MonadSnap m => CI ByteString -> (ByteString -> Bool) -> m ()
    guardHeader h p = getsRequest (getHeader h) >>= maybe pass (guard . p)

    withHeader :: MonadSnap m => CI ByteString -> (ByteString -> m a) -> m a
    withHeader h m = getsRequest (getHeader h) >>= maybe pass m

    withCookie :: MonadSnap m => ByteString -> (Cookie -> m a) -> m a
    withCookie c m = getCookie c >>= maybe pass m

What do you think?

These would be nice additions. I am still unsure about pass though. If I use many guards in my handler action and one of them fails, it is hard to know which one. And then I am still facing the issue of having to send out appropriate error responses to the client, or?

Thanks,
Toralf

Reply all
Reply to author
Forward
0 new messages