obsidiansystems / obelisk

Functional reactive web and mobile applications, with batteries included.
https://reflex-frp.org
BSD 3-Clause "New" or "Revised" License
957 stars 106 forks source link

Question on obelisk routing #361

Closed benkolera closed 5 years ago

benkolera commented 5 years ago

I've got a question about the obelisk routing. I need to encode the following routes:

And while I have no problems making an encoder where there is a dynamic text part at the leaf of the GADT tree (e.g. the article route), I'm a little stuck when the dynamic bit happens in a non leaf (e.g. the profile route). I'm not sure whether it is my encoding of the routes, whether I'm missing an important combinator or Obelisk just doesn't handle this yet.

Here are the data types that I defined to model this.

newtype DocumentSlug = Username { unDocumentSlug :: Text } deriving (Eq, Ord, Show)
makeWrapped ''DocumentSlug
newtype Username = Username { unUsername :: Text } deriving (Eq, Ord, Show)
makeWrapped ''Username

data FrontendRoute :: * -> * where
  FrontendRoute_Home :: FrontendRoute ()
  FrontendRoute_Article :: FrontendRoute DocumentSlug
  FrontendRoute_Profile :: FrontendRoute (Username, Maybe (R ProfileRoute))

data ProfileRoute :: * -> * where
  ProfileRoute_Favourites :: ProfileRoute ()

Which makes sense to me, but I get stuck when I go to try to make the encoder for FrontendRoute_Profile.

backendRouteEncoder
  :: Encoder (Either Text) Identity (R (Sum BackendRoute (ObeliskRoute FrontendRoute))) PageName
backendRouteEncoder = handleEncoder (const (InL BackendRoute_Missing :/ ())) $
  pathComponentEncoder $ \case
    InL backendRoute -> case backendRoute of
      BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty
    InR obeliskRoute -> obeliskRouteSegment obeliskRoute $ \case
      FrontendRoute_Home -> PathEnd $ unitEncoder mempty
      FrontendRoute_Article -> PathSegment "article" $ singlePathSegmentEncoder . unwrappedEncoder
      FrontendRoute_Profile -> PathSegment "profile" $ unicorn

unicorn :: Encoder check parse (Username, Maybe (R ProfileRoute)) PageName
unicorn = pathDynamicComponent unwrappedEncoder profileRMayEnc
  where
    profileRMayEnc :: (MonadError Text check, check ~ parse) => Encoder check parse (Maybe (R ProfileRoute)) PageName
    profileRMayEnc = maybeEncoder (unitEncoder mempty) $ pathComponentEncoder $ \case
      ProfileRoute_Favourites -> PathEnd $ unitEncoder mempty
    -- It feels like you want something that can peel a pathPart off as text, have an encoder for that
    -- part and then an encoder for the entire remaining PageName
    pathDynamicComponent 
      :: MonadError Text parse 
      => Encoder check parse a Text 
      -> Encoder check parse b PageName 
      -> Encoder check parse (a,b) PageName
    pathDynamicComponent = undefined

This looks pretty much like pathComponentEncoderImpl except that we have a tuple rather than a GADT/DSum to represent the pair of values (which makes sense, because our path part is an open set rather than the closed set that the GADTs represent).

I think that I want to write something like the type of my made-up pathDynamicComponent function above. It'd look a lot like chainEncoder, I think, but tupley instead of GADTy. But I can't do that because I don't have access to unEncoder to be able to implement it outside obelisk. Which prompted me to ask y'all here because this kind of route structure is super common and I imagine that the issue is me rather than it not being possible.

P.s - I'm hitting this trying to implement the routes for conduit (the 'real world' demo app). So getting this nice here will serve as a good set of doco for those who come after me. :slightly_smiling_face:

I hope that this all makes sense!

benkolera commented 5 years ago

For those that come later. There is a new function (introduced in #357, which was merged after I started down this path) called:

pathSegmentEncoder :: (MonadError Text parse, Applicative check) =>
  Encoder check parse (Text, PageName) PageName

It doesn't do exactly what I implemented in #362: it's actually better because it is more general. You can use it together with the following bifunctor instance (because we want further encoding of the Text and the sub PageName):

instance (Applicative check, Monad parse) => Bifunctor (,) (Encoder check parse) (Encoder check parse) (Encoder check parse) 

Like so:

      -- Make sure that you haven't got (.) imported from prelude and make sure you have
      -- Control.Categorical.Bifunctor.bimap instead of somewhere else...
      FrontendRoute_Profile -> PathSegment "profile" $
        let profileRouteEncoder = pathComponentEncoder $ \case
              ProfileRoute_Favourites -> PathSegment "favourites" $ unitEncoder mempty
        -- This ends up with an Encoder check parse (Username, Maybe (R ProfileRoute))
        in ( pathSegmentEncoder . bimap unwrappedEncoder (maybeEncoder (unitEncoder mempty) profileRouteEncoder ) )

Thanks to @danbornside for writing pathSegmentEncoder in #357 ! :slightly_smiling_face: