npm package: @simspace/trout
Description: (**t**ypesafe **rout**ing)
Latest version: 0.0.6

# 🐟 trout

(**t**ypesafe **rout**ing)

## Install

### NPM

`npm install @simspace/trout`

### Yarn

`yarn add @simspace/trout`

## Making a route

To make a route, you must either extend an existing path, or extend the `RootPath`. You do this with the `path` function, passing the segments that you wish to extend.

```ts
import * as tr from 'trout'

const usersRoute = pipe(
  tr.RootPath,
  tr.path('users')
)
```

Here, `usersRoute` represents the path: `/users`.

## Route parameters

We can add route parameters by using the `param` function:

```ts
import * as tr from 'trout'

const userProfileRoute = pipe(
  usersRoute,
  tr.param('userId', tr.stringRC)
)
```

Here, we've added a route parameter named `userId`, and passed the `stringRC` route codec to indicate that this parameter's type is a `string`. When encoding and decoding this route, `trout` will use `encodeURIComponent` and `decodeURIComponent` respectively to ensure any string passed into the route constructor is serializable as a valid URL string. More complex codecs (for non-`string` types) can be built upon this, as we'll see below.

## Query parameters

We can add query parameters to a route with the `queryParam`/`queryParamOp` functions:

```ts
const usersRoute = pipe(
  tr.RootPath,
  tr.path('users'),
  tr.queryParam('page', tr.numberRC),
  tr.queryParamOp('pageSize', tr.numberRC)
)
```

The `queryParam` function takes the name of the query parameter, and the codec, which determines the type of the parameter. Here, we expect to get a parameter named `page` which will be attempted to be decoded as a `number`. `queryParamOp` works similarly to `queryParam` except that it specifies an _optional_ query parameter (encoded using `Option` from `fp-ts`).

## Building URLs

Now that we have a route, we can construct URLs from it:

```ts
tr.encodeUrl(usersRoute)({
  query: {
    page: 5,
    pageSize: O.none
  }
}) // gives us: "/users?page=5"
```

The `encodeUrl` function ensures we have the correct types for all of our parameters.

## Extending existing routes

A route can be extended further with more segments via the `path` (or `param`) function. Adding another path segment will invalidate all query parameters from the previous route. Here, `usersRoute` has a `page` parameter, but since `userProfileRoute` added another route segment (via `param`), those query parameters are not included in `userProfileRoute`.

```ts
const userProfileRoute = pipe(
  usersRoute,
  tr.param('userId', tr.stringRC)
)
```

## Route Codecs

To encode and decode parameters in a typesafe manner, `trout` uses route _codecs_. `trout` provides a number of basic codecs out of the box:

1. `stringRC` for valid URL `string`s
2. `numberRC` for `number`s (excluding `NaN` and `bigint`s)
3. `booleanRC` for `boolean`s
4. `stringLiteralRC` and `numberLiteralRC` for literal `string` and `number` values (or unions of those values)
5. `dateRC` for `Date`s (encoded in the URL as a string in simplified extended ISO format, the result of calling `Date.prototype.toISOString()`)

There are also a few useful codec combinators for building more complex codecs:

4. `newtypeRC` takes an `Iso` for the `Newtype` (à la `newtype-ts`), and an underlying route codec, will map the codec's type into a `Newtype`
5. `arrayRC` takes a route codec, and returns a route codec for a homogenous array of those values (only for query parameters, represented in the URL as comma-separated values)
5. `tupleRC` takes a pair of codecs, and will encode and decode a two-element array as a tuple
6. `literalRC` takes a set of literal values and an `Eq` instance for those values, will encode and decode as a union of those values (`stringLiteralRC` and `numberLiteralRC` are implemented with `literalRC`)

`trout`'s `RouteCodec` is implemented in terms of `io-ts/Codec`, so combinators from that library may also be used in constructing custom codecs.

The `newtypeRC` route codec can be used to decode parameters with a supplied `iso` function. Here's an example where the `userId` parameter is specified as a `UserIdM` (instead of just a `string`):

```ts
interface UserIdM extends Newtype<{readonly UserIdM: unique symbol}, string> {}
const isoUserId = iso<UserIdM>()

const userProfilePath = pipe(
  usersRoute,
  tr.param(
    'userId',
    tr.newtypeRC(isoUserId, tr.stringRC)
  )
)
```

Now, when we try to construct this route, we need to supply a `UserIdM`:

```ts
tr.encodeUrl(userProfilePath)({
  route: {
    page: 5,
    userId: isoUserId.wrap('bob')
  }
})
```

## Decoding values from a URL

The `decodeUrl` function takes a route and an URL string, giving us back an `Either`, having a `Left` of errors (if the URL doesn't match this route), or a `Right` of the decoded values:

```ts
const result1 = tr.decodeUrl(
  userProfilePath
)('/users/5/bob')
// right({
//   routeParams: {
//     page: 5,
//     userId: 'bob'
//   },
//   queryParams: {}
// })

const result2 = tr.decodeUrl(
  userProfilePath
)('/users/bob')
// left('Path segment count mismatch')

const result3 = tr.decodeUrl(
  userProfilePath
)('/user/5/bob')
// left('Path segment users does not match user')
```

## Usage with React Router

The `getRouteString` function will provide a `react-router`-compatible string which can be used in the `path` prop of a react router `Route` component.

```tsx
<Route>
  <Route route={tr.getRouteString(usersRoute)} component={UsersList} />
  <Route route={tr.getRouteString(userProfilePath)} component={UsersList} />
</Route>
```

Inside the component, you can decode the URL by combining the location with the search values:

```ts
const UsersList = ({location}: UsersListProps) => {
  const decoded = tr.decodeUrl(usersRoute)(location.pathname + location.search)
  
  ...
}
```