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…
Transformer Stack
You can basically find-and-replace all of these:
Into these:
Lifting
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:
It can be simplified to just:
Which is way better.
The function 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.
Subsite Declaration
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:
CommentsAdmin/Routes.hs
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:
CommentsAdmin.hs
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.
Subsite Actions
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.
Persistent Fields
If you 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 Markdown
type.
Persistent Actions
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 ())
To this:
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.