LoginSignup
2
1

More than 5 years have passed since last update.

Simple Routing based on parsing type-level strings

Posted at

Last time, I talked about Parsing type-level strings to extract types using some new features in PureScript 0.12 to be able to deconstruct Symbols. This gives me back functions of String -> Either _ { | params } that I can use to parse URLs for parameters. But what if I wanted to do some routing?

This time, I'll write about a simple way to do routing by putting these routes into a row type, giving us back a Variant that we can match on.

Row of Types

While we would like to make rows directly of # Symbol, we should remember that RowToList and its kind RowList are Type-kinded, so we cannot use RowToList on a # Symbol. However, by using SProxy, we can carry around Symbols to use in # Type. And just to make this easier on us, we can define a type alias to use:

type RouteURL = SProxy

Top level

So at the top level, what we need is a matchRoutes function that will take in some kind of proxy (usually RProxy) of our RouteURL (url :: Symbol) fields to solve from these determined types the resulting Variant:

matchRoutes
  :: forall proxy routes routesL var
   . RL.RowToList routes routesL
  => RoutesLToVariant routesL var
  => proxy routes
  -> String
  -> Either NoMatch (Variant var)
matchRoutes _ = routesLToVariant (RLProxy :: RLProxy routesL)

where NoMatch is just a newtype of String, so parsing a given URL string with the function returned from providing the routes proxy will end up with either the error for no matching routes or a Variant of the route parameters. So as RowToList gives us our routes row type in iterable form, this is all we need to then solve for var :: # Type.

RoutesLToVariant

Like usual, our type class will have a RowList parameter used to determine the # Type parameter:

class RoutesLToVariant
  (routesL :: RL.RowList)
  (var :: # Type)
  | routesL -> var
  where
    routesLToVariant
      :: RLProxy routesL
      -> String
      -> Either NoMatch (Variant var)

So that only routesL here is used to match instances, like we want.

First, let's look at the base instance when we've exhausted this list:

instance nilRoutesToVariant :: RoutesLToVariant RL.Nil ()
  where
    routesLToVariant _ s = Left (NoMatch s)

Here we have reached the Nil, so we can mark the row as being empty, as there are no more possible members to inject. Then we can just return our non-matched side with NoMatch.

For the Cons case, first let's look at the instance head:

instance consRoutesToVariant ::
  ( K.ParseURL url row
  , IsSymbol rName
  , RoutesLToVariant rTail var'
  , Row.Cons rName { | row } var' var
  , Row.Union var' var'' var
  ) => RoutesLToVariant
         (RL.Cons rName (SProxy url) rTail)
         var

By using the ParseURL class we defined last time, we can get out the row type of the parameter record from the URL, i.e. ParseURL "/hello/{bill:String}" (bill :: String). From there, we can get the remaining variant row from the remaining RowList, and use Row.Cons to add our field of rName and { | row } to the remaining row. The other constraints exist to aid some operations we need: we need to constrain rName with IsSymbol and declare that the remaining variant is some sub-type row for which an inferred complement will form the whole variant row.

And so, when we can define the instance method:

    routesLToVariant _ s =
      case K.parseURL (SProxy :: SProxy url) s of
        Right (r :: { | row }) ->
          Right $ Variant.inj (SProxy :: SProxy rName) r
        Left l ->
          Variant.expand <$> routesLToVariant (RLProxy :: RLProxy rTail) s

And the body here is the same as our usage last time, where if we have a match, we can return the variant member by injecting it with rName, and we can continue on with the remaining RowList to get back our sub-type variant, which we can map over and expand to be of the same type.

And that's it!

Usage

First, let's set up a row of URLs we want to match on:

type RouteURLs =
  ( hello :: RouteURL "/hello/{name}"
  , age :: RouteURL "/age/{age:Int}"
  , answer :: RouteURL "/gimme/{item:String}/{count:Int}"
  )

When we then use this with matchRoutes, the inferred type will be concrete:

main = do
  let
    -- inferred type:
    -- (matchRoutes' :: String
    --     -> Either
    --          NoMatch
    --          (Variant
    --             ( hello :: { name :: String}
    --             , age :: { age :: Int }
    --             , answer :: { item :: String , count :: Int }
    --             )
    --         )
    -- ) =
    matchRoutes' =
      matchRoutes (RProxy :: RProxy RouteURLs)

And from there, it's just a matter of applying a bunch of inputs and matching against them:

    testRoutes =
      [ "/hello/Bill"
      , "/age/12"
      , "/gimme/Apple/24"
      , "/no/match"
      ]

    results = matchRoutes' <$> testRoutes
    handleResult = case _ of
      Left (NoMatch l) ->
        log $ "no match for: " <> show l
      Right r ->
        Variant.match
          { hello: \x -> log $ "hello your name is " <> x.name
          , age: \x -> log $ "hello you are " <> show x.age
          , answer: \x -> log $ "you want " <> show x.count <> " of " <> x.item
          }
          r

  traverse_ handleResult results

  -- result:
  -- hello your name is Bill
  -- hello you are 12
  -- you want 24 of Apple
  -- no match for: "/no/match"

And as expected, our results came back where the three valid routes were matched with the correct types and handled by the Variant.match, and our invalid case came out on the left.

Conclusion

Hopefully this has shown you that even with the new features in 0.12, we can reapply the same techniques from 0.11.x to seamlessly weave together different features to solve our problems.

Links

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1