Skip to content

Commit

Permalink
rework OrMatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
Bessonov committed Apr 17, 2024
1 parent 1c748f4 commit 9b23089
Show file tree
Hide file tree
Showing 19 changed files with 231 additions and 64 deletions.
19 changes: 19 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Breaking changes
----------------
- ESM only.
- `RegExpUrlMatcher` and `EndpointMatcher` don't match query string anymore, but only path, like `ExactUrlPathnameMatcher` do.

Features
--------
- `AndMatcher` and `OrMatcher` supports now up to five matchers instead of two.
- Every matcher can be instantiated with a shortcut function now. For example, `and`, `or`, `method`, `endpoint`, etc.

What's next?
------------
Some ideas:
- `RegExpUrlMatcher` and `EndpointMatcher` will probably get an additional parameter to describe the data instead of do it with type arguments and get only string. This parameter probably will looks like `{userId: numberValidator()}`.
- `ExactQueryMatcher` will probably get validators too. This will improve extensibility a lot. In addition, it will probably handle PHP style arrays such as `users[]=1&users[]=2`.

Trying out
----------
While this state is highly experimental, you can try it out with `pnpm add githubgithub:Bessonov/node-http-router#next`.
4 changes: 2 additions & 2 deletions src/__tests__/Router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
Router,
} from '../Router'
import {
AndMatcher,
type AndMatcherResult,
EndpointMatcher,
type ExactUrlPathnameMatchResult,
Expand All @@ -24,6 +23,7 @@ import {
type MethodMatchResult,
MethodMatcher,
RegExpUrlMatcher,
and,
} from '../matchers'
import type {
ServerRequest,
Expand Down Expand Up @@ -108,7 +108,7 @@ it('match POST /test route', () => {
}

router.addRoute({
matcher: new AndMatcher([
matcher: and([
new MethodMatcher(['POST']),
new ExactUrlPathnameMatcher(['/test']),
]),
Expand Down
19 changes: 19 additions & 0 deletions src/matchers/AndMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,22 @@ MR5
}
}
}

export function and<MR1 extends MatchResultAny | never = never,
P1 = unknown,
MR2 extends MatchResultAny | never = never,
P2 = unknown,
MR3 extends MatchResultAny | never = never,
P3 = unknown,
MR4 extends MatchResultAny | never = never,
P4 = unknown,
MR5 extends MatchResultAny | never = never,
P5 = unknown>(matchers: [
Matcher<MR1, P1>?,
Matcher<MR2, P2>?,
Matcher<MR3, P3>?,
Matcher<MR4, P4>?,
Matcher<MR5, P5>?
]): AndMatcher<MR1, P1, MR2, P2, MR3, P3, MR4, P4, MR5, P5> {
return new AndMatcher(matchers)
}
4 changes: 4 additions & 0 deletions src/matchers/BooleanMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ export class BooleanMatcher<T extends boolean> implements Matcher<MatchResult<T>
}
}
}

export function bool<T extends boolean>(value: T): BooleanMatcher<T> {
return new BooleanMatcher(value)
}
12 changes: 10 additions & 2 deletions src/matchers/EndpointMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
MethodMatcher,
} from './MethodMatcher'
import {
AndMatcher,
type AndMatcher,
and,
} from './AndMatcher'
import {
type RegExpExecGroupArray,
Expand Down Expand Up @@ -53,7 +54,7 @@ implements Matcher<EndpointMatchResult<R>, P> {
>
constructor(methods: Method | Method[], url: RegExp) {
this.match = this.match.bind(this)
this.matcher = new AndMatcher([
this.matcher = and([
new MethodMatcher(Array.isArray(methods) ? methods : [methods]),
new RegExpUrlMatcher<R, P>([url]),
])
Expand All @@ -79,3 +80,10 @@ implements Matcher<EndpointMatchResult<R>, P> {
}
}
}

export function endpoint<
R extends object,
P extends EndpointMatcherInput = EndpointMatcherInput
>(methods: Method | Method[], url: RegExp): EndpointMatcher<R, P> {
return new EndpointMatcher(methods, url)
}
7 changes: 7 additions & 0 deletions src/matchers/ExactQueryMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,10 @@ implements Matcher<ExactQueryMatchResult<U>, P> {
}
}
}

export function exactQuery<
U extends QueryMatch,
P extends ExactQueryMatcherInput
>(config: U): ExactQueryMatcher<U, P> {
return new ExactQueryMatcher(config)
}
7 changes: 7 additions & 0 deletions src/matchers/ExactUrlPathnameMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,10 @@ implements Matcher<ExactUrlPathnameMatchResult<U>, P> {
}
}
}

export function exactUrlPathname<
U extends [string, ...string[]],
P extends ExactUrlPathnameMatcherInput
>(urls: U): ExactUrlPathnameMatcher<U, P> {
return new ExactUrlPathnameMatcher(urls)
}
13 changes: 10 additions & 3 deletions src/matchers/MethodMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ export class MethodMatcher<
}

match({ req }: MethodMatcherInput): MethodMatchResult<M> {
const { method } = req
if (method && this.methods.indexOf(method as Method) >= 0) {
const { method: httpMethod } = req
if (httpMethod && this.methods.indexOf(httpMethod as Method) >= 0) {
return {
matched: true,
result: {
method: method as M[number],
method: httpMethod as M[number],
},
}
}
Expand All @@ -46,3 +46,10 @@ export class MethodMatcher<
}
}
}

export function method<
M extends Method[],
P extends MethodMatcherInput
>(methods: M): MethodMatcher<M, P> {
return new MethodMatcher(methods)
}
91 changes: 74 additions & 17 deletions src/matchers/OrMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,77 @@
import type {
Matcher,
} from './Matcher'
import {
type MatchResult,
type MatchResultAny,
type Matched,
isMatched,
} from './MatchResult'
import type {
Matcher,
} from './Matcher'

export type OrMatcherResult<MR1 extends MatchResultAny, MR2 extends MatchResultAny> = MatchResult<{
or: [MR1, MR2]
export type OrMatcherResult<
MR1 extends MatchResultAny | never = never,
MR2 extends MatchResultAny | never = never,
MR3 extends MatchResultAny | never = never,
MR4 extends MatchResultAny | never = never,
MR5 extends MatchResultAny | never = never,
> = MatchResult<{
or: [
MR1 extends MatchResultAny ? Matched<MR1> : Partial<void>,
MR2 extends MatchResultAny ? Matched<MR2> : Partial<void>,
MR3 extends MatchResultAny ? Matched<MR3> : Partial<void>,
MR4 extends MatchResultAny ? Matched<MR4> : Partial<void>,
MR5 extends MatchResultAny ? Matched<MR5> : Partial<void>,
]
}>

/**
* Match if at least one matcher matches.
* For completeness both matcher are executed.
*/
export class OrMatcher<MR1 extends MatchResultAny, MR2 extends MatchResultAny, P1, P2>
implements Matcher<OrMatcherResult<MR1, MR2>, P1 & P2> {
constructor(private readonly matchers: [Matcher<MR1, P1>, Matcher<MR2, P2>]) {
export class OrMatcher<
MR1 extends MatchResultAny | never = never,
P1 = unknown,
MR2 extends MatchResultAny | never = never,
P2 = unknown,
MR3 extends MatchResultAny | never = never,
P3 = unknown,
MR4 extends MatchResultAny | never = never,
P4 = unknown,
MR5 extends MatchResultAny | never = never,
P5 = unknown,
>
implements Matcher<OrMatcherResult<
MR1,
MR2,
MR3,
MR4,
MR5
>, P1 & P2 & P3 & P4 & P5> {
constructor(private readonly matchers: [
Matcher<MR1, P1>?,
Matcher<MR2, P2>?,
Matcher<MR3, P3>?,
Matcher<MR4, P4>?,
Matcher<MR5, P5>?
]) {
this.match = this.match.bind(this)
}

match(params: P1 & P2): OrMatcherResult<MR1, MR2> {
const [matcher1, matcher2] = this.matchers

const result1 = matcher1.match(params)
const result2 = matcher2.match(params)

const matched = isMatched(result1) || isMatched(result2)
match(params: P1 & P2 & P3 & P4 & P5): OrMatcherResult<
MR1 | never,
MR2 | never,
MR3 | never,
MR4 | never,
MR5 | never
> {
const results = this.matchers.map(matcher => matcher?.match(params))

if (matched) {
if (results.find(result => result && isMatched(result))) {
return {
matched: true,
result: {
or: [result1, result2],
// @ts-expect-error
or: results,
},
}
}
Expand All @@ -42,3 +80,22 @@ implements Matcher<OrMatcherResult<MR1, MR2>, P1 & P2> {
}
}
}

export function or<MR1 extends MatchResultAny | never = never,
P1 = unknown,
MR2 extends MatchResultAny | never = never,
P2 = unknown,
MR3 extends MatchResultAny | never = never,
P3 = unknown,
MR4 extends MatchResultAny | never = never,
P4 = unknown,
MR5 extends MatchResultAny | never = never,
P5 = unknown>(matchers: [
Matcher<MR1, P1>?,
Matcher<MR2, P2>?,
Matcher<MR3, P3>?,
Matcher<MR4, P4>?,
Matcher<MR5, P5>?
]): OrMatcher<MR1, P1, MR2, P2, MR3, P3, MR4, P4, MR5, P5> {
return new OrMatcher(matchers)
}
7 changes: 7 additions & 0 deletions src/matchers/RegExpUrlMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,10 @@ implements Matcher<RegExpUrlMatchResult<R>, P> {
}
}
}

export function regExpUrl<
R extends object,
P extends RegExpUrlMatcherInput = RegExpUrlMatcherInput
>(urls: RegExp[]): RegExpUrlMatcher<R, P> {
return new RegExpUrlMatcher(urls)
}
14 changes: 7 additions & 7 deletions src/matchers/__tests__/AndMatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import {
createRequest,
} from 'node-mocks-http'
import {
AndMatcher,
BooleanMatcher,
ExactUrlPathnameMatcher,
MethodMatcher,
and,
} from '..'
import type {
ServerRequest,
} from '../../node/ServerRequest'

it('none match', () => {
const result = new AndMatcher([
const result = and([
new MethodMatcher(['POST']),
new ExactUrlPathnameMatcher(['/test']),
]).match({ req: createRequest<ServerRequest>() })
Expand All @@ -23,7 +23,7 @@ it('none match', () => {
})

it('first match, second not', () => {
const result = new AndMatcher([
const result = and([
new MethodMatcher(['GET']),
new ExactUrlPathnameMatcher(['/test']),
]).match({ req: createRequest<ServerRequest>() })
Expand All @@ -39,7 +39,7 @@ it('first not match, but second', () => {
url: '/test',
})

const result = new AndMatcher([
const result = and([
new MethodMatcher(['GET']),
new ExactUrlPathnameMatcher(['/test']),
]).match({ req })
Expand All @@ -54,7 +54,7 @@ it('both match', () => {
url: '/test',
})

const result = new AndMatcher([
const result = and([
new MethodMatcher(['GET']),
new ExactUrlPathnameMatcher(['/test']),
]).match({ req })
Expand Down Expand Up @@ -85,7 +85,7 @@ it('three not a match', () => {
url: '/test',
})

const result = new AndMatcher([
const result = and([
new MethodMatcher(['GET']),
new ExactUrlPathnameMatcher(['/test']),
new BooleanMatcher(false),
Expand All @@ -101,7 +101,7 @@ it('three match', () => {
url: '/test',
})

const result = new AndMatcher([
const result = and([
new MethodMatcher(['GET']),
new ExactUrlPathnameMatcher(['/test']),
new BooleanMatcher(true),
Expand Down
6 changes: 3 additions & 3 deletions src/matchers/__tests__/BooleanMatcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
BooleanMatcher,
bool,
} from '..'

it('match', () => {
const result = new BooleanMatcher(true)
const result = bool(true)
.match()
expect(result).toStrictEqual({
matched: true,
Expand All @@ -12,7 +12,7 @@ it('match', () => {
})

it('not match', () => {
const result = new BooleanMatcher(false)
const result = bool(false)
.match()
expect(result).toStrictEqual({
matched: false,
Expand Down
Loading

0 comments on commit 9b23089

Please sign in to comment.