Skip to content

Commit e8cba28

Browse files
committed
Allowed RegExp in the Path
1 parent 857262e commit e8cba28

File tree

8 files changed

+100
-28
lines changed

8 files changed

+100
-28
lines changed

packages/commons/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export {
2222
isNullOrUndefined,
2323
isNumber,
2424
isRecord,
25+
isRegExp,
2526
isStrictEqual,
2627
isString,
2728
isStringUndefinedNullEmpty,

packages/commons/src/typeUtils.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ const isStringUndefinedNullEmpty = (value: unknown) => {
176176
return false;
177177
};
178178

179+
/**
180+
* Check if a Regular Expression
181+
*
182+
* @example
183+
* ```typescript
184+
* import { isRegExp } from '@aws-lambda-powertools/commons/typeUtils';
185+
*
186+
* const value = /^foo.+$/;
187+
* if (isRegExp(value)) {
188+
* // value is a Regular Expression
189+
* }
190+
* ```
191+
*
192+
* @param value - The value to check
193+
*/
194+
const isRegExp = (value: unknown): value is RegExp => {
195+
return value instanceof RegExp;
196+
};
197+
179198
/**
180199
* Get the type of a value as a string.
181200
*
@@ -337,6 +356,7 @@ export {
337356
isNull,
338357
isNullOrUndefined,
339358
isStringUndefinedNullEmpty,
359+
isRegExp,
340360
getType,
341361
isStrictEqual,
342362
};

packages/commons/tests/unit/typeUtils.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
isNullOrUndefined,
77
isNumber,
88
isRecord,
9+
isRegExp,
910
isStrictEqual,
1011
isString,
1112
isStringUndefinedNullEmpty,
@@ -224,6 +225,30 @@ describe('Functions: typeUtils', () => {
224225
});
225226
});
226227

228+
describe('Function: isRegExp', () => {
229+
it('returns true when the passed value is a Regular Expression', () => {
230+
// Prepare
231+
const value = /^hello.+$/;
232+
233+
// Act
234+
const result = isRegExp(value);
235+
236+
// Assess
237+
expect(result).toBe(true);
238+
});
239+
240+
it('returns false when the passed value is not a Regular Expression', () => {
241+
// Prepare
242+
const value = 123;
243+
244+
// Act
245+
const result = isRegExp(value);
246+
247+
// Assess
248+
expect(result).toBe(false);
249+
});
250+
});
251+
227252
describe('Function: getType', () => {
228253
it.each([
229254
{

packages/event-handler/src/rest/RouteHandlerRegistry.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ParameterValidationError } from './errors.js';
1111
import { Route } from './Route.js';
1212
import {
1313
compilePath,
14+
getPathString,
1415
resolvePrefixedPath,
1516
validatePathPattern,
1617
} from './utils.js';
@@ -45,8 +46,8 @@ class RouteHandlerRegistry {
4546
}
4647

4748
// Routes with more path segments are more specific
48-
const aSegments = a.path.split('/').length;
49-
const bSegments = b.path.split('/').length;
49+
const aSegments = getPathString(a.path).split('/').length;
50+
const bSegments = getPathString(b.path).split('/').length;
5051

5152
return bSegments - aSegments;
5253
}
@@ -104,7 +105,7 @@ class RouteHandlerRegistry {
104105

105106
const compiled = compilePath(route.path);
106107

107-
if (!/^[\w+/:-]+$/.test(compiled.path)) {
108+
if (route.path instanceof RegExp) {
108109
if (this.#regexRoutes.has(route.id)) {
109110
this.#logger.warn(
110111
`Handler for method: ${route.method} and path: ${route.path} already exists. The previous handler will be replaced.`
@@ -227,7 +228,7 @@ class RouteHandlerRegistry {
227228
#processRoute(route: DynamicRoute, method: HttpMethod, path: Path) {
228229
if (route.method !== method) return;
229230

230-
const match = route.regex.exec(path);
231+
const match = route.regex.exec(getPathString(path));
231232
if (!match) return;
232233

233234
const params = match.groups || {};

packages/event-handler/src/rest/utils.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { isRecord, isString } from '@aws-lambda-powertools/commons/typeutils';
1+
import {
2+
isRecord,
3+
isRegExp,
4+
isString,
5+
} from '@aws-lambda-powertools/commons/typeutils';
26
import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
37
import type {
48
CompiledRoute,
@@ -15,13 +19,21 @@ import {
1519
UNSAFE_CHARS,
1620
} from './constants.js';
1721

22+
export function getPathString(path: Path): string {
23+
return isString(path) ? path : path.source.replace(/\\\//g, '/');
24+
}
25+
1826
export function compilePath(path: Path): CompiledRoute {
1927
const paramNames: string[] = [];
2028

21-
const regexPattern = path.replace(PARAM_PATTERN, (_match, paramName) => {
22-
paramNames.push(paramName);
23-
return `(?<${paramName}>[${SAFE_CHARS}${UNSAFE_CHARS}\\w]+)`;
24-
});
29+
const pathString = getPathString(path);
30+
const regexPattern = pathString.replace(
31+
PARAM_PATTERN,
32+
(_match, paramName) => {
33+
paramNames.push(paramName);
34+
return `(?<${paramName}>[${SAFE_CHARS}${UNSAFE_CHARS}\\w]+)`;
35+
}
36+
);
2537

2638
const finalPattern = `^${regexPattern}$`;
2739

@@ -36,9 +48,10 @@ export function compilePath(path: Path): CompiledRoute {
3648
export function validatePathPattern(path: Path): ValidationResult {
3749
const issues: string[] = [];
3850

39-
const matches = [...path.matchAll(PARAM_PATTERN)];
40-
if (path.includes(':')) {
41-
const expectedParams = path.split(':').length;
51+
const pathString = getPathString(path);
52+
const matches = [...pathString.matchAll(PARAM_PATTERN)];
53+
if (pathString.includes(':')) {
54+
const expectedParams = pathString.split(':').length;
4255
if (matches.length !== expectedParams - 1) {
4356
issues.push('Malformed parameter syntax. Use :paramName format.');
4457
}
@@ -200,13 +213,22 @@ export const composeMiddleware = (middleware: Middleware[]): Middleware => {
200213
/**
201214
* Resolves a prefixed path by combining the provided path and prefix.
202215
*
216+
* The function returns a RegExp if any of the path or prefix is a RegExp.
217+
* Otherwise, it returns a `/${string}` type value.
218+
*
203219
* @param path - The path to resolve
204220
* @param prefix - The prefix to prepend to the path
205221
*/
206222
export const resolvePrefixedPath = (path: Path, prefix?: Path): Path => {
207-
if (prefix) {
208-
if (!path.startsWith('/')) return `${prefix}/${path}`;
209-
return path === '/' ? prefix : `${prefix}${path}`;
223+
if (!prefix) return path;
224+
if (isRegExp(prefix)) {
225+
if (isRegExp(path)) {
226+
return new RegExp(`${getPathString(prefix)}/${getPathString(path)}`);
227+
}
228+
return new RegExp(`${getPathString(prefix)}${path}`);
229+
}
230+
if (isRegExp(path)) {
231+
return new RegExp(`${prefix}/${getPathString(path)}`);
210232
}
211-
return path;
233+
return `${prefix}${path}`.replace(/\/$/, '') as Path;
212234
};

packages/event-handler/src/types/rest.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ type HttpMethod = keyof typeof HttpVerbs;
6464

6565
type HttpStatusCode = (typeof HttpStatusCodes)[keyof typeof HttpStatusCodes];
6666

67-
type Path = string;
67+
type Path = `/${string}` | RegExp;
6868

6969
type RestRouteHandlerOptions = {
7070
handler: RouteHandler;
@@ -79,9 +79,10 @@ type RestRouteOptions = {
7979
middleware?: Middleware[];
8080
};
8181

82-
type NextFunction =
83-
() => // biome-ignore lint/suspicious/noConfusingVoidType: To ensure next function is awaited
84-
Promise<HandlerResponse | void> | HandlerResponse | void;
82+
type NextFunction = () => // biome-ignore lint/suspicious/noConfusingVoidType: To ensure next function is awaited
83+
| Promise<HandlerResponse | void>
84+
| HandlerResponse
85+
| void;
8586

8687
type Middleware = (args: {
8788
reqCtx: RequestContext;

packages/event-handler/tests/unit/rest/Router/basic-routing.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,15 +200,15 @@ describe('Class: Router - Basic Routing', () => {
200200
])('routes %s %s to %s handler', async (path, method, expectedApi) => {
201201
// Prepare
202202
const app = new Router();
203-
app.get('/files/.+', async () => ({ api: 'serveFile' }));
204-
app.get('/files/.+', async () => ({ api: 'serveFileOverride' }));
205-
app.get('/api/v\\d+/.*', async () => ({ api: 'apiVersioning' }));
206-
app.get('/users/:userId/files/.+', async (reqCtx) => ({
203+
app.get(/\/files\/.+/, async () => ({ api: 'serveFile' }));
204+
app.get(/\/files\/.+/, async () => ({ api: 'serveFileOverride' }));
205+
app.get(/\/api\/v\d+\/.*/, async () => ({ api: 'apiVersioning' }));
206+
app.get(/\/users\/:userId\/files\/.+/, async (reqCtx) => ({
207207
api: `dynamicRegex${reqCtx.params.userId}`,
208208
}));
209-
app.get('.+', async () => ({ api: 'getAnyRoute' }));
209+
app.get(/.+/, async () => ({ api: 'getAnyRoute' }));
210210
app.route(async () => ({ api: 'catchAllUnmatched' }), {
211-
path: '.*',
211+
path: /.*/,
212212
method: [HttpVerbs.GET, HttpVerbs.POST],
213213
});
214214

packages/event-handler/tests/unit/rest/utils.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,13 +577,15 @@ describe('Path Utilities', () => {
577577
{ path: '/test', prefix: '/prefix', expected: '/prefix/test' },
578578
{ path: '/', prefix: '/prefix', expected: '/prefix' },
579579
{ path: '/test', expected: '/test' },
580-
{ path: '.+', prefix: '/prefix', expected: '/prefix/.+' },
580+
{ path: /.+/, prefix: '/prefix', expected: /\/prefix\/.+/ },
581+
{ path: '/test', prefix: /\/prefix/, expected: /\/prefix\/test/ },
582+
{ path: /.+/, prefix: /\/prefix/, expected: /\/prefix\/.+/ },
581583
])('resolves prefixed path', ({ path, prefix, expected }) => {
582584
// Prepare & Act
583585
const resolvedPath = resolvePrefixedPath(path as Path, prefix as Path);
584586

585587
// Assert
586-
expect(resolvedPath).toBe(expected);
588+
expect(resolvedPath).toEqual(expected);
587589
});
588590
});
589591
});

0 commit comments

Comments
 (0)