58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
export type PathMatcher = (url: string) => string | null;
|
|
|
|
/** Strips origin, query string, hash and trailing slashes from a URL or path. */
|
|
export function getPathname(url: string): string {
|
|
const raw = /^https?:\/\//i.test(url) ? new URL(url).pathname : url.split(/[?#]/)[0];
|
|
|
|
return raw.length > 1 ? raw.replace(/\/+$/, '') : raw;
|
|
}
|
|
|
|
/**
|
|
* Maps a concrete request path back to an OpenAPI path template.
|
|
*
|
|
* Candidates must have the same number of segments. A literal segment match
|
|
* scores higher than a `{param}` match, so `/api/websites/charts` resolves to
|
|
* `/api/websites/charts` rather than `/api/websites/{websiteId}`, and
|
|
* `/api/reports/funnel` beats `/api/reports/{reportId}`.
|
|
*/
|
|
export function createMatcher(templates: string[]): PathMatcher {
|
|
const candidates = [...new Set(templates)].map(template => ({
|
|
template,
|
|
segments: template.split('/').filter(Boolean),
|
|
}));
|
|
|
|
return url => {
|
|
const segments = getPathname(url).split('/').filter(Boolean);
|
|
let best: { template: string; score: number } | null = null;
|
|
|
|
for (const candidate of candidates) {
|
|
if (candidate.segments.length !== segments.length) {
|
|
continue;
|
|
}
|
|
|
|
let score = 0;
|
|
let matches = true;
|
|
|
|
for (let i = 0; i < segments.length; i++) {
|
|
const expected = candidate.segments[i];
|
|
const actual = segments[i];
|
|
|
|
if (expected === actual) {
|
|
score += 1;
|
|
} else if (expected.startsWith('{') && expected.endsWith('}') && actual) {
|
|
// parameter segment: matches anything non-empty
|
|
} else {
|
|
matches = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (matches && (!best || score > best.score)) {
|
|
best = { template: candidate.template, score };
|
|
}
|
|
}
|
|
|
|
return best?.template ?? null;
|
|
};
|
|
}
|