{"id":13709609,"url":"https://github.com/natefaubion/purescript-routing-duplex","last_synced_at":"2026-02-04T18:01:17.976Z","repository":{"id":33277061,"uuid":"146226407","full_name":"natefaubion/purescript-routing-duplex","owner":"natefaubion","description":"Unified parsing and printing for routes in PureScript","archived":false,"fork":false,"pushed_at":"2023-06-15T14:09:42.000Z","size":67,"stargazers_count":94,"open_issues_count":10,"forks_count":18,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-10-07T02:27:40.317Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PureScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/natefaubion.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2018-08-27T00:11:28.000Z","updated_at":"2025-08-20T13:19:53.000Z","dependencies_parsed_at":"2024-01-08T14:30:45.576Z","dependency_job_id":"7b390f1a-2d5c-4aa0-a9cc-9d21d5ee2334","html_url":"https://github.com/natefaubion/purescript-routing-duplex","commit_stats":{"total_commits":33,"total_committers":13,"mean_commits":"2.5384615384615383","dds":0.5757575757575757,"last_synced_commit":"13702d4a73ac3eac1d8c09701fb06faca7073728"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/natefaubion/purescript-routing-duplex","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natefaubion%2Fpurescript-routing-duplex","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natefaubion%2Fpurescript-routing-duplex/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natefaubion%2Fpurescript-routing-duplex/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natefaubion%2Fpurescript-routing-duplex/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/natefaubion","download_url":"https://codeload.github.com/natefaubion/purescript-routing-duplex/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/natefaubion%2Fpurescript-routing-duplex/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29092707,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-04T03:31:03.593Z","status":"ssl_error","status_checked_at":"2026-02-04T03:29:50.742Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-02T23:00:42.423Z","updated_at":"2026-02-04T18:01:17.958Z","avatar_url":"https://github.com/natefaubion.png","language":"PureScript","readme":"Simple bidirectional parser/printers for your routing data types.\n\n# Why?\n\nStrongly-typed languages let you define your routes as a data type, ensuring invalid routes fail to compile. But the browser represents locations as strings, so you have to write functions to decode strings into your routing data type and functions to write a route to a string.\n\nUnfortunately, writing separate functions to parse and print your routing data type is error-prone and boilerplate-heavy. It’s easy to update a parser and forget to update the accompanying printer, even though almost all routing definitions should round-trip (parsing then printing returns the original string value).\n\n`routing-duplex` takes an approach that solves both problems. This library lets you define a codec, or a means to both decode and encode a particular data type, for your routes. Write this codec once and it will handle parsing and printing the same representation for you.\n\n## A Brief Example\n\nLet’s build a codec for a simple app with two routes: the homepage and user profiles (identified by usernames).\n\n1. Write a data type to represent our two routes, deriving `Generic`.\n2. Build a codec using generics and combinators from `Routing.Duplex`\n\n```purescript\nimport Data.Generic.Rep (class Generic)\nimport Routing.Duplex (RouteDuplex', path, root, segment, string)\nimport Routing.Duplex.Generic as G\n\ndata Route = Home | Profile String\n\nderive instance genericRoute :: Generic Route _\n\nroute :: RouteDuplex' Route\nroute = root $ G.sum\n  { \"Home\": G.noArgs\n  , \"Profile\": path \"profile\" (string segment)\n  }\n```\n\nWith our codec in place we can parse browser locations (strings) into our routing data type:\n\n```shell\nparse route \"/\"\n\u003e Right Home\n\nparse route \"/profile/jake-delhomme\"\n\u003e Right (Profile \"jake-delhomme\")\n```\n\nAnd we can also serialize our routing data type into browser locations:\n\n```shell\nprint route $ Profile \"jake-delhomme\"\n\u003e \"/profile/jake-delhomme\"\n```\n\n# How to use this library\n\n`routing-duplex` works by letting you define a codec which represents how to encode and decode your routing data type. You can define your routing data however you see fit, and then provide it to the library’s codec type, `RouteDuplex`:\n\n```purescript\ndata RouteDuplex i o = RouteDuplex (i -\u003e RoutePrinter) (RouteParser o)\n```\n\nIntuitively you can think of this as a data type that describes how you can parse a browser location into a route or print a route as a browser location. This type is used for low-level combinators and records and allows you to print / parse a different input and output. However, a proper codec for a routing data type would be bidirectional: it should parse and print the _same_ type, your `Route`. For that reason, you will generally use the `RouteDuplex’` type instead:\n\n```purescript\ntype RouteDuplex' a = RouteDuplex a a\n```\n\nThis library exports a number of helper functions and combinators for constructing this codec with minimal boilerplate, mostly concentrated in two modules:\n\n- `Routing.Duplex` exports the `RouteDuplex` type, the `print` and `parse` functions, and combinators that represent constants (`“/post“`), segments (`/:id`), parameters (`?foo=`), prefixes and suffixes, optional values, and more.\n- `Routing.Duplex.Generic` exports helpers for deriving code via your data type’s `Generic` instance, most notably the `sum` function for describing a route as a sum type and the `product` and `noArgs` functions for working with product types.\n- `Routing.Duplex.Generic.Syntax` exports some symbols that can be used to write terse codecs similar to those found in string-based routers.\n\n# Examples\n\nWe’ll explore several practical examples of this library in practice while building a real-world routing data type and codec.\n\n## Example: Writing a codec for a sum type\n\nLet’s begin developing a more complex set of routes. We’ll design routes for a small blogging site made up of users and their posts, as well as feed showing new posts from across the site. We can represent these routes in a small data type:\n\n```purescript\ndata Route\n  = Root\n  | Profile String\n  | Post String Int\n  | Feed\n\nderive instance genericRoute :: Generic Route _\n```\n\nNext, we can define our codec. First, we’ll use the `root` combinator to match on an opening slash:\n\n```purescript\n-- Will match '/', and then anything further specified by the codec\n-- it is passed as an argument.\nroot :: forall a b. RouteDuplex a b -\u003e RouteDuplex a b\n```\n\nNext, we need to represent the ability to parse static and dynamic segments from a path. Paths are always separated by slashes (like `user/5/favorites`), where each string between slashes is considered a segment (like `”user\"`, `”5”`, and `”favorites\"`. Sometimes we need to match a segment exactly, like matching the segment `”user\"`, but at other times we want to capture the value of a segment that could have many different values. Here, we have a dynamic segment representing a user ID that we’d like to capture.\n\nWe can use the `path`, `segment`, and `param` helper functions to capture these static and dynamic segments.\n\n```purescript\n-- Allows you to match a static segment. For example, to match the\n-- /feed path, use `path \"feed\" ...` where `...` represents further\n-- segments.\npath :: forall a b. String -\u003e RouteDuplex a b -\u003e RouteDuplex a b\n\n-- Allows you to capture a variable segment. For example, the path\n-- /user/5 has one static segment and one variable segment. This always\n-- reads and writes a string, but when used with other combinators, it\n-- can transform that string into a type of your choice. For example:\n-- `path \"user\" (int segment)` parses to `User Int`.\nsegment :: RouteDuplex' String\n\n-- Allows you to capture a query parameter. For example, the path\n-- /user?foo=bar has one query parameter and could be represented\n-- with `param \"foo\"`.\nparam :: String -\u003e RouteDuplex' String\n```\n\nWhile the path you work with is always a `String`, you can transform that input into your routing data type by using parser combinators from the library. We’ve just seen an example of one of them, `int`, but there are several more, including:\n\n```purescript\nstring :: RouteDuplex' String -\u003e RouteDuplex' String\nboolean :: RouteDuplex' String -\u003e RouteDuplex' Boolean\nint :: RouteDuplex' String -\u003e RouteDuplex' Int\noptional :: forall a b. RouteDuplex a b -\u003e RouteDuplex (Maybe a) (Maybe b)\n```\n\nYou can easily implement your own combinators using `as`, the function used to construct each of the built-in combinators. We’ll see an example of that later on!\n\nAt this point, we have the tools we need to:\n\n- Handle static segments of a path, like `\"/user\"`\n- Handle variable segments of a path, like `/:username` or `/:postid`\n- Handle query parameters, like `?foo=bar`\n- Transform string segments into other types, like using the `int` combinator to turn a post ID into a String\n\nHowever, we still need two more tools to construct our codec. First, we need to be able to specify codecs for every case in our routing sum type: this can be done with the `sum` function from the `Routing.Duplex.Generic` module. Second, we need to be able to specify that a type should match zero or more of these dynamic segments. For routes that have no arguments, we’ll use `noArgs`; for routes with one argument, we’ll just provide a codec; and for routes with multiple arguments, we’ll combine codecs with `product`.\n\nLet’s see all of this in action!\n\nFirst, we’ll use the `root` combinator to match a leading slash. Then, we’ll use the `sum` function to specify that we’re matching a sum type (our `Route` data type):\n\n```purescript\nroute = root $ sum\n  { ... }\n```\n\n`sum` exposes a nice record syntax so that we can specify codecs for each constructor of the type; if you forget to handle a constructor, you’ll get a compiler error. We need to write a few codecs:\n\n- The `Root` constructor takes no arguments and should match when the path is empty. We can represent that with a simple `noArgs`.\n- The `Profile` constructor takes one argument, a string `Username`, and should match a path that begins with `”user“`. We can represent that using the `path` and `segment` functions, along with the `string` combinator.\n- The `Post` constructor takes two arguments: a string `Username` and an integer `PostId`. We’ll need to use the `product` function to put two dynamic segments together.\n- The `Feed` constructor should only match a string constant in the path, `”feed”`. We can represent that with the `path` function and `noArgs`.\n\n```purescript\nroute = root $ sum\n  { \"Root\": noArgs\n  , \"Profile\": path \"user\" (string segment)\n  , \"Post\":\n      product\n        (path \"user\" (string segment))\n        (path \"post\" (int segment))\n  , \"Feed\": path \"feed\" noArgs\n  }\n```\n\nIt can be a little awkward using `product` for complex routes, so there’s also an operator version, `(/)`, which is more convenient to use infix. We can also omit the `string` combinator because all segments are strings by default. With this in mind, let’s revise our `”Post”` case:\n\n```purescript\n  {\n  , \"Post\": path \"user\" segment / path \"post\" (int segment)\n  }\n```\n\nIn fact, when we’re matching string constants, we can omit the call to `path` and just provide the string directly:\n\n```purescript\n  {\n  , \"Post\": \"user\" / segment / \"post\" / int segment\n  }\n```\n\n## Example: Working with optional or required query params\n\nUsers need to be able to search their feeds. This information will come via query parameters, which will be optional. We haven’t dealt with optional segments or query params so far, but they’re easy to add.\n\nFirst, let’s adjust our route type so that it can accommodate query parameters. Query parameters have a key:value pairing, so it’s typical to represent them with a record type.\n\n```purescript\ndata Route\n  = Root\n  | Profile String\n  | Post String Int\n  | Feed { search :: Maybe String }\n```\n\nWe’ll have to update our codec so that `Feed` takes a record as an argument. We can do this manually with the `record` function and its `:=` operator, which lets you assign a key in the record to a particular codec. Record keys are type-level strings, so we’ll need to use `Proxy` to create them.\n\nIntuitively, we can read the below codec as “Match `”feed”` and then, if it exists, a query parameter with the key “search”, storing its value at the key “search” in the output record.” This time, we'll use the `optional` combinator to represent an optional value:\n\n```purescript\nroute = root $ sum\n  { ...\n  , \"Feed\": path \"feed\" (record # _search := optional (param \"search\"))\n  }\n  where\n    _search = Proxy :: Proxy \"search\"\n```\n\nThis explicit record creation can be done any time you have a record in your route type. However, using a record for query parameters is common enough that this library exports a helper function, `params`, which lets you just provide a record of codecs where the record keys are treated as the query param keys, too. There's also an operator version of `params`, `(?)`. We could rewrite our above codec using this helper function:\n\n```purescript\n  { ...\n  , \"Feed\": path \"feed\" $ params { search: optional \u003c\u003c\u003c string }\n  -- alternately\n  , \"Feed\": \"feed\" / params { search: optional \u003c\u003c\u003c string }\n  -- alternately\n  , \"Feed\": \"feed\" ? { search: optional \u003c\u003c\u003c string }\n  }\n```\n\nAt this point, our codec is looking much cleaner:\n\n```purescript\nroute = root $ sum\n  { \"Root\": noArgs\n  , \"Profile\": \"user\" / segment\n  , \"Post\": \"user\" / segment / \"post\" / int segment\n  , \"Feed\": \"feed\" ? { search: optional \u003c\u003c\u003c string }\n  }\n```\n\n## Example: Defining a new codec for custom data\n\nUnfortunately, our route data type is not as type-safe as we’d like it to be. We aren’t really parsing just string and ints — we’re dealing with `Username`s and `PostId`s. In addition, we’ve had a last-minute request to allow users to choose how to sort posts in their feed. We’ll need a custom data type for that, too.\n\nOur codec can easily handle our custom data types. We just have to make our own combinator that describes how to transform to and from a string. In fact, the primitive combinators we saw before (`int`, `boolean`, `string`, `optional`, etc.) are all built using a helper function, `as`, which we can leverage as well.\n\n```purescript\n-- Note: the actual function is more polymorphic, but here I've specialized\n-- some types to how you will almost always use them in practice.\nas\n  :: forall a\n   . (a -\u003e String)\n  -\u003e (String -\u003e Either String a)\n  -\u003e RouteDuplex' String\n  -\u003e RouteDuplex a\n```\n\nThe `as` function allows you to produce your own combinator. In short, you are responsible for providing a function from your custom type to a `String`, and one from a string segment to either an error or your custom type. That leaves one argument free, which will be some existing codec that you are augmenting to produce your custom type. For example, consider the partially-applied `as` for our type vs. the `int` combinator:\n\n```purescript\nas' :: forall a. RouteDuplex' String -\u003e RouteDuplex' a\nint ::           RouteDuplex' String -\u003e RouteDuplex' Int\n```\n\nLet’s use this to revise our routing data type. First, let’s define a way to represent sorting, and functions that convert to and from strings for the data type.\n\n```purescript\ndata Sort = Asc | Desc\n\nderive instance genericSort :: Generic Sort _\n\nsortToString :: Sort -\u003e String\nsortToString = case _ of\n  Asc -\u003e \"asc\"\n  Desc -\u003e \"desc\"\n\nsortFromString :: String -\u003e Either String Sort\nsortFromString = case _ of\n  \"asc\" -\u003e Right Asc\n  \"desc\" -\u003e Right Desc\n  val -\u003e Left $ \"Not a sort: \" \u003c\u003e val\n```\n\nWith these functions in place, it’s trivial to write a new combinator for our sorting data type:\n\n```purescript\nsort :: RouteDuplex' String -\u003e RouteDuplex' Sort\nsort = as sortToString sortFromString\n```\n\nLet’s put this to use! We’ll add `Sort` as a new query parameter, and we’ll use our new combinator to update our route codec, too.\n\n```purescript\ndata Route\n  = Root\n  | Profile String\n  | Post String Int\n  | Feed { search :: Maybe String, sorting :: Maybe Sort }\n\nderive instance genericRoute :: Generic Route _\n\nroute :: RouteDuplex' Route\nroute = root $ sum\n  { ...\n  , \"Feed\": \"feed\" ? { search: optional, sorting: optional \u003c\u003c\u003c sort }\n  }\n```\n\nNext, lets make our `Route` data type better by providing newtypes to uniquely identify a string as a `Username` or an int as a `PostId`.\n\n```purescript\nnewtype Username = Username String\nderive instance newtypeUsername :: Newtype Username _\n\nnewtype PostId = PostId Int\nderive instance newtypePostId :: Newtype PostId _\n\ndata Route\n  = Root\n  | Profile Username\n  | Post Username PostId\n  | Feed { search :: Maybe String, sorting :: Maybe Sort }\n```\n\nWe could write new combinators as we did for the `Sort` data type, but we don’t really have brand-new data types here. We have newtypes around a `String` and `Int`, which already have combinators available, so we ought to re-use them. This re-use is trivial if we use the `_Newtype` iso from `purescript-profunctor-lenses` along with the existing combinators to create our two new codecs:\n\n```purescript\nuname :: RouteDuplex' Username\nuname = _Newtype segment\n\n-- re-use the `int` combinator\npostId :: RouteDuplex' PostId\npostId = _Newtype (int segment)\n```\n\nFinally, we can replace our earlier segments with these new codecs.\n\n```purescript\nroute :: RouteDuplex' Route\nroute = root $ sum\n  { \"Root\": noArgs\n  , \"Profile\": \"user\" / uname\n  , \"Post\": \"user\" / uname / \"post\" / postId\n  , \"Feed\": \"feed\" ? { search: optional, sorting: optional \u003c\u003c\u003c sort }\n  }\n```\n\n## Example: Composing codecs to represent CRUD operations\n\nWe’ve seen the `RouteDuplex’ a` type all over the place, whether to represent a small codec for integers or strings or a larger one for our complex sum type. We can create codecs of any size and compose them into larger structures. Let’s walk through an example by extending our routing data type to accommodate create, read, and update operations for posts in our system.\n\nFirst, we’ll define a data type to represent creating, reading, and updating a resource dependent on some kind of identifier, `a`:\n\n```purescript\ndata CRU a\n  = Create\n  | Read a\n  | Update a\n\nderive instance genericCRU :: Generic (CRU a) _\n```\n\nNext, we’ll again use the `sum` function to write a codec for this sum type. We don’t know how to handle `a`, so we’ll accept a codec to handle it as an argument. We’d like to handle three cases:\n\n- `/` should represent creation\n- `/:id` should represent reading\n- `/edit/:id` should represent updating\n\nExactly the same way we wrote a codec for our `Route` type we can write one for our new `CRU` type:\n\n```purescript\ncru :: forall a. RouteDuplex' a -\u003e RouteDuplex' (CRU a)\ncru inner = sum\n  { \"Create\": noArgs\n  , \"Read\": inner\n  , \"Update\": \"edit\" / inner\n  }\n```\n\nEven better, we can use our new data type as part of our `Route` type to describe a resource that follows this URL structure:\n\n```purescript\ndata Route\n  = ...\n  | Post Username (CRU PostId)\n```\n\nAnd re-use our codec to produce the larger `Route` codec:\n\n```purescript\nroute = root $ sum\n  { ...\n  , \"Post\": \"user\" / uname / \"post\" / cru postId\n  }\n```\n\n## Example: Running our codec with `purescript-routing`\n\nWe've developed a capable parser and printer for our route data type. To be useful, though, we'll want to use our parser along with a library that handles hash-based or pushState routing for us. The most common choice is the `purescript-routing` library. If you aren't familiar with how the library works, [consider skimming the official guide](https://github.com/slamdata/purescript-routing/blob/v8.0.0/GUIDE.md).\n\nWe'll use the library to handle hashes and pushState, but rather than use their parser combinators, we'll provide our own, custom parser -- our codec.\n\nFirst, we'll choose the `matchesWith` function that fits our use case:\n\n- [`Routing.Hash.matchesWith`](https://pursuit.purescript.org/packages/purescript-routing/8.0.0/docs/Routing.Hash#v:matchesWith)\n- [`Routing.PushState.matchesWith`](https://pursuit.purescript.org/packages/purescript-routing/8.0.0/docs/Routing.PushState#v:matchesWith)\n\nFrom here, we'll assume hash-based routing. Next, we'll take a look at the type signature of `matchesWith`. In a nutshell, this function expects a custom parser and a function that will accept a (possible) previous route and the route that just matched and perform some effects with them. It returns an effect that can be used to remove the event listener this will create.\n\n```purescript\nmatchesWith :: forall f a. Foldable f =\u003e (String -\u003e f a) -\u003e (Maybe a -\u003e a -\u003e Effect Unit) -\u003e Effect (Effect Unit)\n```\n\nOur custom parser will be the `parse` function from `Routing.Duplex` given our codec:\n\n```purescript\nparse :: forall i o. RouteDuplex i o -\u003e String -\u003e Either Parser.RouteError o\n```\n\nFilling in the types with our `Route` data type, we get:\n\n```purescript\nmatchesWith :: (String -\u003e Either RouteError Route) -\u003e (Maybe Route -\u003e Route -\u003e Effect Unit) -\u003e Effect (Effect Unit)\n```\n\nTo perform your routing effects, provide your custom callback function:\n\n```purescript\ncanceller \u003c- matchesWith (parse route) \\old new -\u003e do\n  ... your routing effects, called every time the route changes ...\n```\n","funding_links":[],"categories":["URL Routers","Http"],"sub_categories":["Http routing"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnatefaubion%2Fpurescript-routing-duplex","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnatefaubion%2Fpurescript-routing-duplex","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnatefaubion%2Fpurescript-routing-duplex/lists"}