Recently, Yesod released version 1.2. You can read the announcement here, the changelog here, and a detailed blog post about the subsite rewrite here. These resources do a great job of getting users’ apps up on 1.2. This post won’t rehash those, it is instead intended for those of you (like myself) who maintain libraries dependent on Yesod.
The large refactor to the transformer stack and its implications in how subsites are written made it non-trivial to port my markdown, comments, and pagination libraries over to 1.2; I imagine many other such authors are in the same position and might appreciate a little guidance.
I don’t claim to know why or how all this stuff works, but at least you can benefit from my trial and error.
I apologize for the lack of narrative or conclusion here, this is pretty much just a list of things I had to take care of during the update process…
You can basically find-and-replace all of these:
fooHandler :: X -> Y -> GHandler s m a fooWidget :: X -> Y -> GWidget s m a
fooHandler :: X -> Y -> HandlerT m IO a fooWidget :: X -> Y -> WidgetT m IO a
Anywhere you use
lift to run a
Handler action from within a
Widget now needs to use
handlerToWidget for the same purpose.
Route to Master
Subsites and their masters are now very well isolated, this means you no longer need code like this in a master site’s hander:
tm <- getRouteToMaster redirect $ tm SomeRoute
It can be simplified to just:
Which is way better.
getRouteToMaster does still exist as
getRouteToParent, and it should be used (only) to route to a master site’s route from within the subsite’s handler.
If you author a subsite, here is where your largest changes will be. There’s a handy demo app which serves as a great reference.
Subsites now have a two-phase construction much like in
Foundation.hs. So, where you might’ve had a single module like this:
module CommentsAdmin ( CommentsAdmin , getCommentsAdmin , Route(..) ) where CommentsAdmin = CommentsAdmin getCommentsAdmin :: a -> CommentsAdmin getCommentsAdmin = const CommentsAdmin mkYesodSub "CommentsAdmin" [ ClassP ''YesodComments [ VarT $ mkName "master" ] ] [parseRoutes| / CommentsR GET /edit/#ThreadId/#CommentId EditCommentR GET POST /delete/#ThreadId/#CommentId DeleteCommentR GET POST |]
You now need a separate file to define the routes:
module CommentsAdmin.Routes where CommentsAdmin = CommentsAdmin mkYesodSubData "CommentsAdmin" [parseRoutes| / CommentsR GET /edit/#ThreadId/#CommentId EditCommentR GET POST /delete/#ThreadId/#CommentId DeleteCommentR GET POST |]
And import/use them separately:
module Foo ( CommentsAdmin , getCommentsAdmin , module CommentsAdmin.Routes ) where import CommentsAdmin.Routes getCommentsAdmin :: a -> CommentsAdmin getCommentsAdmin = const CommentsAdmin instance YesodComments m => YesodSubDispatch CommentsAdmin (HandlerT m IO) where yesodSubDispatch = $(mkYesodSubDispatch resourcesCommentsAdmin)
There’s probably a way around this, but I had enough wrestling to do.
You’ll also want to make a
Handler synonym for your subsite routes:
type Handler a = forall master. YesodComments master => HandlerT CommentsAdmin (HandlerT master IO) a getCommentsR :: Handler RepHtml
It’s fine to use
Handler as long as you don’t export it.
What you do from within a subsite will definitely need some tweaking, but that’s mostly because the old way was very klunky and the new way is much cleaner.
If you want to call any functions in the context of the main site, just use
lift. Usually, this’ll be
lift $ defaultLayout, but also, as may be common, if you have a Typeclass on your master site providing some functionality (like loading comments), you need to use
lift to call those functions from within subsite handlers.
derive PersistField also now
derive PersistFieldSql. I don’t know the motivation behind the split, but as a user dog-fooding my own library, I soon realized I needed both instances on my
If you have a library exposing functions which are meant to be called within
runDB, you probably already know those type signatures can get messy.
Well, they stay messy, but at least I can tell you what you need to change. Mine went from this:
selectPaginated :: ( PersistEntity val , PersistQuery m1 , PersistEntityBackend val ~ PersistMonadBackend m1 , MonadLift (GHandler s m) m1 => Int -> [Filter val] -> [SelectOpt val] -> m1 ([Entity val], GWidget s m ())
selectPaginated :: ( PersistEntity val , (PersistQuery (YesodPersistBackend m (HandlerT m IO))) , (PersistMonadBackend (YesodPersistBackend m (HandlerT m IO)) ~ PersistEntityBackend val) , (MonadTrans (YesodPersistBackend m)) , Yesod m ) => Int -> [Filter val] -> [SelectOpt val] -> YesodDB m ([Entity val], WidgetT m IO ())
I probably could’ve added the
Yesod m constraint and used the
YesodDB alias prior to 1.2, but oh well.