Skip to content

Commit

Permalink
enchance matchers
Browse files Browse the repository at this point in the history
  • Loading branch information
Bessonov committed Apr 21, 2024
1 parent bab99c2 commit b125c0f
Show file tree
Hide file tree
Showing 53 changed files with 1,342 additions and 482 deletions.
7 changes: 7 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ module.exports = {
'error',
'ignorePackages',
],
'no-labels': [
'error',
{
allowLoop: true,
},
],
'no-continue': 'off',
// disable styling rules
'max-len': 'off',
'sort-imports': 'off',
Expand Down
23 changes: 0 additions & 23 deletions CHANGES.md

This file was deleted.

75 changes: 48 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ This router is intended to be used with native node http interface. Features:
- Written in TypeScript with focus on type safety.
- Extensible via [`Matcher`](src/matchers/Matcher.ts) and [`MatchResult`](src/matchers/MatchResult.ts) interfaces.
- Works with [native node http server](#usage-with-native-node-http-server).
- Works with [micro](#usage-with-micro).
- Works with [micro](#usage-with-micro), and, probably, with any other server.
- Offers a set of matchers:
- [`MethodMatcher`](#methodmatcher)
- [`ExactUrlPathnameMatcher`](#exacturlpathnamematcher)
- [`QueryStringMatcher`](#querystringmatcher)
- Powerful [`RegExpUrlMatcher`](#regexpurlmatcher)
- Powerful [`RegExpPathnameMatcher`](#regexppathnamematcher)
- Convenient [`EndpointMatcher`](#endpointmatcher)
- `AndMatcher` and `OrMatcher`
- Can be used with [path-to-regexp](https://github.com/pillarjs/path-to-regexp).
Expand Down Expand Up @@ -52,7 +52,7 @@ The router doesn't depends on the native http interfaces like `IncomingMessage`
#### Usage with native node http server

```typescript
const router = new NodeHttpRouter()
const router = new NodeHttpRouter(({ data: { res } }) => send(res, 404))

const server = http.createServer(router.serve).listen(8080, 'localhost')

Expand All @@ -61,11 +61,6 @@ router.addRoute({
handler: () => 'Hello kitty!',
})

// 404 handler
router.addRoute({
matcher: bool(true),
handler: ({ data: { res } }) => send(res, 404)
})
```

See [full example](src/examples/node.ts) and [native node http server](https://nodejs.org/api/http.html#http_class_http_server) documentation.
Expand All @@ -80,20 +75,14 @@ import {
serve,
} from 'micro'

const router = new NodeHttpRouter()
const router = new NodeHttpRouter(({ data: { res } }) => send(res, 404))

http.createServer(serve(router.serve)).listen(8080, 'localhost')

router.addRoute({
matcher: exactUrlPathname(['/hello']),
handler: () => 'Hello kitty!',
})

// 404 handler
router.addRoute({
matcher: bool(true),
handler: ({ data: { res } }) => send(res, 404)
})
```

See [full example](src/examples/micro.ts).
Expand Down Expand Up @@ -136,7 +125,7 @@ eventRouter.addRoute({

// add default handler
eventRouter.addRoute({
matcher: bool(true),
matcher: bool(true), // matches everything
handler({ data }) {
return `the event '${data.name}' is unknown`
}
Expand Down Expand Up @@ -168,7 +157,7 @@ router.addRoute({
#### ExactUrlPathnameMatcher
([source](./src/matchers/ExactUrlPathnameMatcher.ts))

Matches given pathnames (but ignores query parameters):
Matches the given pathnames:

```typescript
router.addRoute({
Expand Down Expand Up @@ -210,32 +199,64 @@ See [all provided validators](src/validators.ts).

```

#### RegExpUrlMatcher
([source](./src/matchers/RegExpUrlMatcher.ts))
#### RegExpPathnameMatcher
([source](./src/matchers/RegExpPathnameMatcher.ts))

Allows powerful expressions:

```typescript
router.addRoute({
matcher: regExpUrl<{ userId: string }>([/^\/group\/(?<userId>[^/]+)$/]),
handler: ({ match: { result: { match } } }) => `User id is: ${match.groups.userId}`,
matcher: regExpPathname([/^\/group\/(?<userId>[^/]+)$/], {
userId: chain(getNthVal(), requiredVal(), toNumVal()),
}),
handler: ({ match: { result: { pathname } } }) => `User id is: ${pathname.params.userId}`,
})
```
Be aware that regular expression must match the whole base url (also with query parameters) and not only `pathname`. Ordinal parameters can be used too.
Ordinal parameters can be used too.

#### EndpointMatcher
([source](./src/matchers/EndpointMatcher.ts))

EndpointMatcher is a combination of Method and RegExpUrl matcher for convenient usage:
EndpointMatcher is a combination of Method, RegExpPathname, and QueryString matchers for convenient usage:

```typescript
router.addRoute({
matcher: endpoint<{ userId: string }>('GET', /^\/group\/(?<userId>[^/]+)$/),
handler: ({ match: { result: { method, match } } }) => `Group id ${match.groups.userId} matched with ${method} method`,
matcher: endpoint('GET', /^\/user\/(?<userId>[^/]+)$/
{
url: {
userId: chain(getNthVal(), requiredVal(), toNumVal()),
},
query: {
// profile qery parameter is optional in this example
profile: chain(atLeastOneVal([undefined, 'short', 'full']), getNthVal()),
},
},
),
handler: ({ match: { result: { pathname, query } } }) => {
return `User id: ${pathname.params.userId}, profile: ${query.profile}.`,
}
})
```

Both, `RegExpUrlMatcher` and `EndpointMatcher` can be used with a string for an exact match instead of a RegExp.
`EndpointMatcher` can be used with a string for an exact match instead of a RegExp.

### Validators
([source](./src/validators.ts))

Validators set expectations on the input and convert values. Currently, they work with `RegExpPathnameMatcher`, `EndpointMatcher`, and `QueryStringMatcher`. Following validators are available:

Validator | Array | Value | Description
----------------|-------|-------|------------
`trueVal` | v | v | It's a wildcard validator mainly used for optional parameters.
`requiredVal` | v | v | Validator to mark required parameters.
`getNthVal` | v | x | Returns the n-th element of an array. A negative value returns the element counting from the end of the array.
`mapToNumVal` | v | x | Ensures that a string array can be converted to a number array.
`toNumVal` | x | v | Ensures that a single value can be converted to a number.
`atLeastOneVal` | v | x | Filters the passed values based on the listed options. If `undefined` is included in the option list, it indicates that the passed value is optional.
`oneOfVal` | x | v | Ensures that the passed value is one of the listed options.
`minCountVal` | v | x | Ensures that the passed array has a minimal length.

Validators can be chained with the `chain` function.

### Middlewares

Expand Down Expand Up @@ -364,7 +385,7 @@ router.addRoute({
### Nested routers

There are some use cases for nested routers:
- Add features like multi-tenancy
- Features like multi-tenancy
- Implement modularity
- Apply middlewares globally

Expand Down
103 changes: 103 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
About the 3.0.0 Release
=========================
First things first, I advise against updating to this release unless you specifically need the new features or the required changes are minimal. This release is more conceptual, intended for experimentation and gathering feedback about the new validators. I have a strong sense that there are significant, exciting developments on the horizon that could render matchers in their current form obsolete. Let's wait for the next release to see.

However, this release brings a lot of power and capabilities that you may find interesting. Read on for more details.

Breaking Changes
================

ESM Only
--------
As more and more packages migrate to ESM-only builds, this release follows suit.

EndpointMatcher
----------------
The `EndpointMatcher` no longer matches the query string; it now matches only the path, similar to `ExactUrlPathnameMatcher`. To match query parameters, you can use the `query` field in the third argument. Additionally, type arguments are no longer used. Instead, use the `url` field in the third parameter to describe your RegExp output.

Before:
```typescript
new EndpointMatcher<{ name: string }>('GET', /^\/hello\/(?<name>[^/]+)$/)
```

After:
```typescript
endpoint('GET', /^\/hello\/(?<name>[^/]+)$/, {
url: {
name: chain(getNthVal(), requiredVal()),
},
query: {
myparam: getNthVal(),
},
})
```

Features
========

Validators
----------
`RegExpPathnameMatcher`, `EndpointMatcher`, and `QueryStringMatcher` can now be used with validators. In short, a validator is a set of functions that determine if arguments can be transformed to some format and transform them on demand. Many useful validators are already provided; check the README.md for details.

AndMatcher and OrMatcher
------------------------
`AndMatcher` and `OrMatcher` now support up to five matchers instead of two.

Shortcut Functions
------------------
Every matcher, except those deprecated, can now be instantiated with a shortcut function. For example, `and`, `or`, `method`, `endpoint`, etc.

EndpointMatcher
---------------
`EndpointMatcher` now supports plain strings for paths instead of only RegExp. They match the URL as is. Internally, the string is converted to RegExp. At least for now, there are no plans to support parameters within strings.

Deprecations
============

RegExpUrlMatcher
----------------
Use the new `RegExpPathnameMatcher` matcher. Similar to `EndpointMatcher`, it matches only the `pathname` instead of the whole URL and has a third parameter.

Before:
```typescript
new RegExpUrlMatcher<{ groupId: string }>([/^\/group\/(?<groupId>[^/]+)$/])
```

After:
```typescript
regExpPathname([/^\/group\/(?<groupId>[^/]+)$/], {
groupId: chain(getNthVal(), requiredVal(), toNumVal()),
})
```

ExactQueryMatcher
-----------------
`ExactQueryMatcher` performs its job well, but it wasn't suitable for certain scenarios. For example, it couldn't be used to match multiple query string parameters with the same name, such as `userIds[]=1&userIds[]=2`. Additionally, the output was often a `string`, and the expressiveness was very limited. The brand new `QueryStringMatcher` resolves such issues.

Before:
```typescript
new ExactQueryMatcher({
mustPresent: true,
mustAbsent: false,
isOptional: undefined,
mustExact: ['exactValue1', 'exactValue2'] as const,
})
```

After:
```typescript
queryString({
mustPresent: chain(getNthVal(), requiredVal()),
// mustAbsent: false, // not possible yet!
isOptional: getNthVal(),
mustExact: chain(atLeastOneVal(['exactValue1', 'exactValue2']), getNthVal(), requiredVal()),
})
```

Note that a negative match isn't possible yet. However, I'm not sure if anyone has ever used it.

Trying It Out
=============
While this state is highly experimental, you can try it out with `pnpm add github:Bessonov/node-http-router#next`.

After that, you can update to the latest version with `pnpm update -r @bessonovs/node-http-router`.
31 changes: 22 additions & 9 deletions dist/__tests__/Router.test.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b125c0f

Please sign in to comment.