- Removed deprecated route utility functions in favor of direct matcher usage. - Updated imports to reflect new module structure for routing utilities. - Consolidated route manager functionality into SharedRouteManager for better consistency. - Eliminated legacy routing methods and interfaces, streamlining the HttpProxy and associated components. - Enhanced WebSocket and HTTP request handling to utilize the new unified HttpRouter. - Updated route matching logic to leverage matcher classes for domain, path, and header checks. - Cleaned up legacy compatibility code across various modules, ensuring a more maintainable codebase.
184 lines
5.3 KiB
TypeScript
184 lines
5.3 KiB
TypeScript
import type { IMatcher, IPathMatchResult } from '../types.js';
|
|
|
|
/**
|
|
* PathMatcher provides comprehensive path matching functionality
|
|
* Supporting exact matches, wildcards, and parameter extraction
|
|
*/
|
|
export class PathMatcher implements IMatcher<IPathMatchResult> {
|
|
/**
|
|
* Convert a path pattern to a regex and extract parameter names
|
|
* Supports:
|
|
* - Exact paths: /api/users
|
|
* - Wildcards: /api/*
|
|
* - Parameters: /api/users/:id
|
|
* - Mixed: /api/users/:id/*
|
|
*/
|
|
private static patternToRegex(pattern: string): {
|
|
regex: RegExp;
|
|
paramNames: string[]
|
|
} {
|
|
const paramNames: string[] = [];
|
|
let regexPattern = pattern;
|
|
|
|
// Escape special regex characters except : and *
|
|
regexPattern = regexPattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
// Handle path parameters (:param)
|
|
regexPattern = regexPattern.replace(/:(\w+)/g, (match, paramName) => {
|
|
paramNames.push(paramName);
|
|
return '([^/]+)'; // Match any non-slash characters
|
|
});
|
|
|
|
// Handle wildcards
|
|
regexPattern = regexPattern.replace(/\*/g, '(.*)');
|
|
|
|
// Ensure the pattern matches from start
|
|
regexPattern = `^${regexPattern}`;
|
|
|
|
// If pattern doesn't end with wildcard, ensure it matches to end
|
|
// But only for patterns that don't have parameters or wildcards
|
|
if (!pattern.includes('*') && !pattern.includes(':') && !pattern.endsWith('/')) {
|
|
regexPattern = `${regexPattern}$`;
|
|
}
|
|
|
|
return {
|
|
regex: new RegExp(regexPattern),
|
|
paramNames
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Match a path pattern against a request path
|
|
* @param pattern The pattern to match
|
|
* @param path The request path to test
|
|
* @returns Match result with params and remainder
|
|
*/
|
|
static match(pattern: string, path: string): IPathMatchResult {
|
|
// Handle null/undefined cases
|
|
if (!pattern || !path) {
|
|
return { matches: false };
|
|
}
|
|
|
|
// Normalize paths (remove trailing slashes unless it's just "/")
|
|
const normalizedPattern = pattern === '/' ? '/' : pattern.replace(/\/$/, '');
|
|
const normalizedPath = path === '/' ? '/' : path.replace(/\/$/, '');
|
|
|
|
// Exact match (most common case)
|
|
if (normalizedPattern === normalizedPath) {
|
|
return {
|
|
matches: true,
|
|
pathMatch: normalizedPath,
|
|
pathRemainder: '',
|
|
params: {}
|
|
};
|
|
}
|
|
|
|
// Pattern matching (wildcards and parameters)
|
|
const { regex, paramNames } = this.patternToRegex(normalizedPattern);
|
|
const match = normalizedPath.match(regex);
|
|
|
|
if (!match) {
|
|
return { matches: false };
|
|
}
|
|
|
|
// Extract parameters
|
|
const params: Record<string, string> = {};
|
|
paramNames.forEach((name, index) => {
|
|
params[name] = match[index + 1];
|
|
});
|
|
|
|
// Calculate path match and remainder
|
|
let pathMatch = match[0];
|
|
let pathRemainder = normalizedPath.substring(pathMatch.length);
|
|
|
|
// Handle wildcard captures
|
|
if (normalizedPattern.includes('*') && match.length > paramNames.length + 1) {
|
|
const wildcardCapture = match[match.length - 1];
|
|
if (wildcardCapture) {
|
|
pathRemainder = wildcardCapture;
|
|
pathMatch = normalizedPath.substring(0, normalizedPath.length - wildcardCapture.length);
|
|
}
|
|
}
|
|
|
|
// Clean up path match (remove trailing slash if present)
|
|
if (pathMatch !== '/' && pathMatch.endsWith('/')) {
|
|
pathMatch = pathMatch.slice(0, -1);
|
|
}
|
|
|
|
return {
|
|
matches: true,
|
|
pathMatch,
|
|
pathRemainder,
|
|
params
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if a pattern contains parameters or wildcards
|
|
*/
|
|
static isDynamicPattern(pattern: string): boolean {
|
|
return pattern.includes(':') || pattern.includes('*');
|
|
}
|
|
|
|
/**
|
|
* Calculate the specificity of a path pattern
|
|
* Higher values mean more specific patterns
|
|
*/
|
|
static calculateSpecificity(pattern: string): number {
|
|
if (!pattern) return 0;
|
|
|
|
let score = 0;
|
|
|
|
// Exact paths are most specific
|
|
if (!this.isDynamicPattern(pattern)) {
|
|
score += 100;
|
|
}
|
|
|
|
// Count path segments
|
|
const segments = pattern.split('/').filter(s => s.length > 0);
|
|
score += segments.length * 10;
|
|
|
|
// Count static segments (more static = more specific)
|
|
const staticSegments = segments.filter(s => !s.startsWith(':') && s !== '*');
|
|
score += staticSegments.length * 20;
|
|
|
|
// Penalize wildcards and parameters
|
|
const wildcards = (pattern.match(/\*/g) || []).length;
|
|
const params = (pattern.match(/:/g) || []).length;
|
|
score -= wildcards * 30; // Wildcards are very generic
|
|
score -= params * 10; // Parameters are somewhat generic
|
|
|
|
// Bonus for longer patterns
|
|
score += pattern.length;
|
|
|
|
return score;
|
|
}
|
|
|
|
/**
|
|
* Find all matching patterns from a list
|
|
* Returns patterns sorted by specificity (most specific first)
|
|
*/
|
|
static findAllMatches(patterns: string[], path: string): Array<{
|
|
pattern: string;
|
|
result: IPathMatchResult;
|
|
}> {
|
|
const matches = patterns
|
|
.map(pattern => ({
|
|
pattern,
|
|
result: this.match(pattern, path)
|
|
}))
|
|
.filter(({ result }) => result.matches);
|
|
|
|
// Sort by specificity (highest first)
|
|
return matches.sort((a, b) =>
|
|
this.calculateSpecificity(b.pattern) - this.calculateSpecificity(a.pattern)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Instance method for interface compliance
|
|
*/
|
|
match(pattern: string, path: string): IPathMatchResult {
|
|
return PathMatcher.match(pattern, path);
|
|
}
|
|
} |